1. Remix IDE 테스트
1.1. AdvancedStorage.test.js
const { expect } = require("chai");
const { ethers } = require("ethers");
describe("AdvancedStorage", function () {
it("Check vault manager", async function () {
// Make sure contract is compiled and artifacts are generated
const advancedStorageMetadata = JSON.parse(await remix.call('fileManager', 'getFile', 'artifacts/AdvancedStorage.json'))
const signer = (new ethers.providers.Web3Provider(web3Provider)).getSigner()
const signerAddress = await signer.getAddress();
let AdvancedStorage = new ethers.ContractFactory(advancedStorageMetadata.abi, advancedStorageMetadata.data.bytecode.object, signer);
let advancedStorage = await AdvancedStorage.deploy();
console.log('storage contract Address: ' + advancedStorage.address);
await advancedStorage.deployed();
expect((await advancedStorage.vaultManager()).toString()).to.equal(signerAddress);
});
it("Check set initial investment", async function () {
const advancedStorageMetadata = JSON.parse(await remix.call('fileManager', 'getFile', 'artifacts/AdvancedStorage.json'));
const customerIdentityCardMetadata = JSON.parse(await remix.call('fileManager', 'getFile', 'artifacts/CustomerIdentityCard.json'));
const provider = new ethers.providers.Web3Provider(web3Provider)
const signer = provider.getSigner();
const acc2 = await provider.getSigner(1).getAddress();
let AdvancedStorage = new ethers.ContractFactory(advancedStorageMetadata.abi, advancedStorageMetadata.data.bytecode.object, signer);
let advancedStorage = await AdvancedStorage.deploy();
console.log('storage contract Address: ' + advancedStorage.address);
await advancedStorage.deployed();
await advancedStorage.setInitialInvestmentVault(10, 5, acc2.toString());
const customerIdentityCardAddress = (await advancedStorage.retrieveInvestmentVault())[3];
const customerIdentityCard = new ethers.Contract(customerIdentityCardAddress, customerIdentityCardMetadata.abi, signer);
expect((await advancedStorage.retrieveInvestmentVault())[1].toNumber()).to.equal(5);
expect((await advancedStorage.retrieveInvestmentVault())[2]).to.equal(true);
expect(customerIdentityCardAddress).to.equal(customerIdentityCard.address);
});
it("Check customer information", async function() {
const advancedStorageMetadata = JSON.parse(await remix.call('fileManager', 'getFile', 'artifacts/AdvancedStorage.json'));
const customerIdentityCardMetadata = JSON.parse(await remix.call('fileManager', 'getFile', 'artifacts/CustomerIdentityCard.json'));
const provider = new ethers.providers.Web3Provider(web3Provider)
const signer = provider.getSigner();
const acc2 = await provider.getSigner(1).getAddress();
let AdvancedStorage = new ethers.ContractFactory(advancedStorageMetadata.abi, advancedStorageMetadata.data.bytecode.object, signer);
let advancedStorage = await AdvancedStorage.deploy();
console.log('storage contract Address: ' + advancedStorage.address);
await advancedStorage.deployed();
await advancedStorage.setInitialInvestmentVault(10, 5, acc2.toString());
const customerIdentityCardAddress = (await advancedStorage.retrieveInvestmentVault())[3];
const customerIdentityCard = new ethers.Contract(customerIdentityCardAddress, customerIdentityCardMetadata.abi, signer);
expect(await customerIdentityCard.customer()).to.equal(acc2);
});
});1.2. AdvancedStorage_test.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
import {AdvancedStorage, CustomerIdentityCard} from "../AdvancedStorage.sol";
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract testSuite is AdvancedStorage {
AdvancedStorage advancedStorage;
address acc0;
address acc1;
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
advancedStorage = new AdvancedStorage();
acc0 = TestsAccounts.getAccount(0);
acc1 = TestsAccounts.getAccount(1);
}
function checkVaultManager() public returns (bool) {
return Assert.equal(this.vaultManager(), msg.sender, "Vault Manager is not correct");
}
function checkSettingInitialInvestment() public returns (bool, bool, bool) {
setInitialInvestmentVault(
10,
5,
acc1
);
return (
Assert.equal(retrieveInvestmentVault().investmentDuration, block.timestamp + 10 days, "Duration is not correct"),
Assert.equal(retrieveInvestmentVault().returnOnInvestment, 5, "Return on Investment is not correct"),
Assert.equal(retrieveInvestmentVault().initialized, true, "Initialization status is not correct")
);
}
/// #sender: account-1
function checkFailedSettingInitialInvestmentButWithUnautorizedAccount() public returns (bool) {
setInitialInvestmentVault(
10,
5,
acc1
);
return (Assert.ok(true, "True"));
}
function checkRetrieveCustomerInformation() public returns (bool) {
return Assert.equal(retrieveCustomerInformation(), acc1, "Customer information is wrong");
}
}
1.3. SimpleStorage_test.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
import "../SimpleStorage.sol";
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract testSuite is SimpleStorage {
SimpleStorage simpleStorage;
address acc0;
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
simpleStorage = new SimpleStorage();
acc0 = TestsAccounts.getAccount(0);
}
function checkMaintainerName() public returns (bool) {
return Assert.equal(simpleStorage.maintainerName(), "zxstim", "Maintainer name is not correct");
}
function checkVersion() public returns (bool) {
return Assert.equal(simpleStorage.version(), 1, "Version is not 1");
}
function checkDonationAddress() public returns (bool) {
return Assert.equal(simpleStorage.donationAddress(), 0xe3d25540BA6CED36a0ED5ce899b99B5963f43d3F, "Donation address is not correct");
}
/// #sender: account-0
function checkStoredPerson() public returns (bool, bool, bool, bool, bool, bool) {
Person memory person = storePerson("victor",30,true,10,2);
return (
Assert.equal(person.name, "victor", "Name is not correct"),
Assert.equal(person.age, 30, "Age is not correct"),
Assert.equal(person.overEighteen, true, "overEighteen status is not correct"),
Assert.equal(person.uuid, msg.sender, "Address is not correct"),
Assert.equal(person.assetValue, 10e18, "Asset value is not correct"),
Assert.equal(person.debtValue, 2e18, "Debt value is not correct")
);
}
/// #sender: account-0
function checkRetrivePersonWithAddress() public returns (bool, bool, bool, bool, bool, bool) {
Assert.ok(msg.sender == acc0, "caller should be default account i.e. acc0");
storePerson("victor",30,true,10,2);
return (
Assert.equal(retrievePerson(msg.sender).name, "victor", "Name is not correct"),
Assert.equal(retrievePerson(msg.sender).age, 30, "Age is not correct"),
Assert.equal(retrievePerson(msg.sender).overEighteen, true, "overEighteen status is not correct"),
Assert.equal(retrievePerson(msg.sender).uuid, msg.sender, "Address is not correct"),
Assert.equal(retrievePerson(msg.sender).assetValue, 10e18, "Asset value is not correct"),
Assert.equal(retrievePerson(msg.sender).debtValue, 2e18, "Debt value is not correct")
);
}
}
2. 솔리디티 파일 테스트
솔리디티(Solidity) 파일의 철저한 테스트는 블록체인 프로젝트의 품질, 신뢰성 및 보안을 보장하는 데 중요한 역할을 합니다.
다음은 몇 가지 주요 이유입니다:
- 오류 탐지: 테스트를 통해 솔리디티 코드의 오류를 파악하고 수정할 수 있습니다. 코드 오류는 의도하지 않은 동작을 유발하거나 심지어 자산 손실로 이어질 수도 있습니다. 테스트를 통해 이러한 오류를 조기에 발견하고 심각한 문제가 발생하기 전에 수정할 수 있습니다.
- 보안 확보: 테스트를 통해 솔리디티 코드의 보안 취약점을 파악할 수 있습니다. 보안 취약점은 프로젝트를 해커의 공격에 노출시킬 수 있습니다. 테스트를 통해 이러한 취약점을 찾아내고 이를 해결하기 위한 조치를 취할 수 있습니다.
- 신뢰성 향상: 테스트를 통해 블록체인 프로젝트의 신뢰성을 높일 수 있습니다. 사용자들은 프로젝트가 철저한 테스트를 거쳤다는 사실을 알게 되면 프로젝트에 대한 신뢰도가 높아지고, 이를 이용할 가능성이 커집니다.
- 시간과 비용을 절약하세요: 테스트를 통해 장기적으로 시간과 비용을 절약할 수 있습니다. 오류를 조기에 수정하면 나중에 더 심각하고 비용이 많이 드는 문제를 예방할 수 있습니다.
솔리디티(Solidity) 파일을 테스트하는 데 사용할 수 있는 다양한 방법이 있습니다. 널리 사용되는 방법으로는 다음과 같은 것들이 있습니다:
- 단위 테스트: 코드의 개별 단위를 테스트하는 방법입니다.
- 통합 테스트: 서로 다른 코드 단위들이 어떻게 함께 작동하는지 테스트하는 방법입니다.
- 포크 테스트: 실제 환경을 모방한 환경에서 코드를 테스트하는 방법입니다.
- 스테이징 테스트: 프로덕션 환경이 아닌 실제 환경에서 코드를 테스트하는 방법입니다. 적합한 테스트 방법을 선택하는 것은 프로젝트의 구체적인 요구 사항에 따라 달라집니다.
솔리디티 파일을 효과적으로 테스트하기 위한 몇 가지 팁은 다음과 같습니다:
- 이해하기 쉽고 유지보수가 용이한 테스트 코드를 작성하십시오.
- 다양한 테스트 방법을 활용하십시오.
- 테스트를 자동화하세요.
- 전문적인 테스트 도구를 사용하는 것을 고려해 보세요.
테스트 방법은?
Remix, Hardhat, Foundry에는 각각 스마트 계약을 테스트할 수 있는 자체 기능이 있습니다. 아래의 상세 문서를 참고하시기 바랍니다:
3. 솔리디티의 계약에 대한 자세한 정보
3.1. 생성자
컨스트럭터는 스마트 계약이 초기화될 때 즉시 실행되는 함수입니다
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Base contract X
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
}3.2. 상태 변수의 가시성
공개- 공용 변수는 다음과 유사합니다내부변수(현재 계약 및 상속받은 계약이 액세스할 수 있도록 허용)이지만, 자동으로게터 함수외부 계약에서도 이에 접근할 수 있도록.내부- 이 변수는 현재 계약과 상속된 계약에서만 접근할 수 있습니다. 이는 상태 변수의 기본 가시성 설정이기도 합니다.비공개- 해당 변수는 현재 계약에서만 접근할 수 있습니다.
참고:
그내부그리고비공개변수는 다른 변수에 대한 접근만 제한합니다계약서. 해당 변수의 값은 모든 사용자에게 계속 표시됩니다.
3.3. 함수의 가시성
외부-함수오직 외부에서만 호출할 수 있는.공개-함수둘 다 다른 쪽에서 호출될 수 있다함수에서계약, 또한 외부에서 호출할 수도 있습니다.내부-함수기존의...에 의해서만 호출될 수 있습니다계약또는 유전된계약.비공개-함수현재의...에 의해서만 호출될 수 있다계약.
3.4. 게터 함수
함수 ~를 호출하는 데 사용됩니다 공개 컴파일러가 자동으로 생성하는 변수. 또한 다음 개념을 가리키는 데도 사용된다. 함수 표시할 변수를 조회하는 데 사용됩니다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}3.5. 상수 및 불변 상태 변수
상수- 컴파일 시 즉시 값이 고정되는 변수(계약 바이트코드에 포함됨).불변의- 값을 할당할 수 있는 변수구성.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals = 18;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint decimals_, address ref) {
if (decimals_ != 0)
// Immutables are only immutable when deployed.
// At construction time they can be assigned to any number of times.
decimals = decimals_;
// Assignments to immutables can even access the environment.
maxBalance = ref.balance;
}
function isBalanceTooHigh(address other) public view returns (bool) {
return other.balance > maxBalance;
}
}3.6. 순수 함수
함수 블록체인의 상태를 읽거나 변경하지 않습니다. 또는 계산에 사용됩니다. 함수.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}3.7. 지불 가능한 기능 및 주소
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Payable {
// Payable address can send Ether via transfer or send
address payable public owner;
// Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}
// Function to deposit Ether into this contract.
// Call this function along with some Ether.
// The balance of this contract will be automatically updated.
function deposit() public payable {}
// Call this function along with some Ether.
// The function will throw an error since this function is not payable.
function notPayable() public {}
// Function to withdraw all Ether from this contract.
function withdraw() public {
// get the amount of Ether stored in this contract
uint256 amount = address(this).balance;
// send all Ether to owner
(bool success,) = owner.call{value: amount}("");
require(success, "Failed to send Ether");
}
// Function to transfer Ether from this contract to address from input
function transfer(address payable _to, uint256 _amount) public {
// Note that "to" is declared as payable
(bool success,) = _to.call{value: _amount}("");
require(success, "Failed to send Ether");
}
}3.8. 이더리움 수령 및 폴백 함수
A 계약 최대 하나까지 가질 수 있다 수신하다 다음과 같이 선언된 함수 receive() external payable { ... } (~ 없이) 함수 (키워드). 이 함수 ~가 없어야 한다 논거, 할 수 없다 돌아가기 무엇이든, 그리고 꼭 있어야 하는 것 외부 가시성뿐만 아니라 지급해야 할 상태의 가변성. 다음과 같을 수 있습니다 가상, 그럴 수 있습니다 재정의 그리고 다음과 같은 경우가 있을 수 있습니다 수식어.
어느 함수 는 호출되는가호출되고, 폴백() 또는 수신()?
보내기 이더
|
메시지.데이터 는 비어?
/ \
예 아니요
/ \
수신() 존재? 대체()
/ \
예 아니요
/ \
수신() 대체()// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Fallback {
event Log(string func, uint256 gas);
// Fallback function must be declared as external.
fallback() external payable {
// send / transfer (forwards 2300 gas to this fallback function)
// call (forwards all of the gas)
emit Log("fallback", gasleft());
}
// Receive is a variant of fallback that is triggered when msg.data is empty
receive() external payable {
emit Log("receive", gasleft());
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
contract SendToFallback {
function transferToFallback(address payable _to) public payable {
_to.transfer(msg.value);
}
function callFallback(address payable _to) public payable {
(bool sent,) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
}3.9. 오라클
오라클은 블록체인과 외부 세계를 연결하는 가교 역할을 합니다. API, 시장 데이터, 기상 데이터 등 블록체인 외부의 다양한 소스에서 스마트 계약에 필요한 데이터를 제공합니다.
다음은 오라클을 스마트 계약에 활용하는 몇 가지 예시입니다:
- 분산형 시장(DeFi)을 위한 가격 데이터 제공: 오라클은 암호화폐 자산의 가격 데이터를 제공하여 트레이더들이 분산형 거래소에서 거래할 수 있도록 합니다.
- 보험 계약 활성화: 오라클은 사고나 자연재해와 같은 보험 관련 사건에 대한 데이터를 제공하여 보험금 지급을 촉발할 수 있습니다.
- 프로세스 자동화: Oracle을 활용하면 청구서 결제나 공급망 관리와 같은 프로세스를 자동화할 수 있습니다.
Klaytn의 오라클 목록: https://klaytn.foundation/ecosystem/?search=&cate=oracles-bridges&sort=abc
4. 파운드리 펀드미
4.1. 프레임워크 파운드리
사실 Remix IDE는 기능 면에서 여러 가지 한계가 있으므로, 스마트 계약을 개발, 테스트 및 배포하기 위해 Foundry라는 프레임워크를 사용할 것입니다.
4.2. 설치
GetFoundry.sh 웹사이트를 방문하여 안내에 따라 진행해 주세요.
4.3. 시작하기
Foundry Book에 접속하여 안내에 따라 프로젝트를 초기화하세요.
4.4. 펀드미 프로젝트
이 연습 문제는 Patrick Collins의 Foundry FundMe 저장소를 기반으로 하지만, Klaytn 환경에 맞게 수정되었습니다.
- 첫 상영
forge init klaytn-fund-me - 그런 다음 다음을 생성하겠습니다.
FundMe.sol파일
// FundMe.sol
// SPDX-License-Identifier: MIT
// 1. Pragma
pragma solidity ^0.8.19;
// 2. Imports
// We import the orakl library so we can interact with oracle
import { IAggregator } from "@bisonai/orakl-contracts/src/v0.1/interfaces/IAggregator.sol";
// We import the PriceConverter library so we can calculate the KLAY value
import { PriceConverter } from "./PriceConverter.sol";
// 3. Interfaces, Libraries, Contracts
// Declaring error is not the Owner of the contract
error FundMe__NotOwner();
/**
* @title A sample Funding Contract
* @author Patrick Collins
* @notice This contract is for creating a sample funding contract
* @dev This implements price feeds as our library
*/
contract FundMe {
// Type Declarations
// The next line means
// use the PriceConverter library for variables with type uint256
using PriceConverter for uint256;
// State variables
// Declare a public constant MINIMUM_USD with value $5 but equal to peb so must multiply by 10^18
uint256 public constant MINIMUM_USD = 5 * 10 ** 18;
// Declare a private and immutable address with the name i_owner, i means immutable.
address private immutable i_owner;
// Declare a private array containing a list of people who fund ether with the name s_funders, s means storage.
address[] private s_funders;
// Declare a mapping between address and private uint256 linking the address with the fund amount.
mapping(address => uint256) private s_addressToAmountFunded;
// Declare contract AggregatorV3Interface private and assign it to the variable s_pricefeed, s means storage
IAggregator private s_priceFeed;
// Events (we have none!)
// Modifiers
// Declare an onlyOwner modifier to assign to a function that only the owner can call
modifier onlyOwner() {
// require(msg.sender == i_owner);
if (msg.sender != i_owner) revert FundMe__NotOwner();
_;
}
// Functions Order:
//// constructor
//// receive
//// fallback
//// external
//// public
//// internal
//// private
//// view / pure
// Declaring a constructor with an address for priceFeed implies that this is the address of the Oracle contract with IAggregator
constructor(address priceFeed) {
// Input the address into the interface and assign it to the variable s_priceFeed
s_priceFeed = IAggregator(priceFeed);
// Assign the variable i_owner to msg.sender (the person who deploys this contract)
i_owner = msg.sender;
}
/// @notice Funds our contract based on the KLAY/USDT price from Orakl
// Deposit to our contract based on ETH/USD price
function fund() public payable {
require(msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
// Then map the sender's address with msg.value in mapping s_addressToAmountFunded
s_addressToAmountFunded[msg.sender] += msg.value;
// Then add the sender address to the list of funders
s_funders.push(msg.sender);
}
function withdraw() public onlyOwner {
// Use for loop, starting from index 0 to index less than the length of the list, and index plus 1 for each loop
for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) {
// assign the address value at funderIndex in the s_funders list to the funder address
address funder = s_funders[funderIndex];
// Change the value of mapping s_addressToAmountFunded whose address is funder to 0, meaning this funder has withdrawn
s_addressToAmountFunded[funder] = 0;
}
// Create a new s_funders list with a new dynamic array (literally a list) of size 0
s_funders = new address[](0);
// Transfer vs call vs Send
// Transfer vs call vs Send
// - transfer (2300 gas, throws error if any)
// - send (2300 gas, returns bool for success or failure)
// - call (forward all gas or set gas, returns bool for success or failure)
// payable(msg.sender).transfer(address(this).balance);
// Send the entire balance of this contract to i_owner with no data in the transaction and return boolean success or not
(bool success,) = i_owner.call{value: address(this).balance}("");
// Require bool success true otherwise revert completely
require(success);
}
function cheaperWithdraw() public onlyOwner {
// Copy the list of s_funders from storage to memory, that is, load from global state to local state. Changing global state consumes more gas than local state
address[] memory funders = s_funders;
// mappings can't be in memory, sorry!
for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
address funder = funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
// payable(msg.sender).transfer(address(this).balance);
(bool success,) = i_owner.call{value: address(this).balance}("");
require(success);
}
/** Getter Functions */
// Functions are only used to GET information
/**
* @notice Gets the amount that an address has funded
* @param fundingAddress the address of the funder
* @return the amount funded
*/
function getAddressToAmountFunded(address fundingAddress) public view returns (uint256) {
return s_addressToAmountFunded[fundingAddress];
}
/**
* @notice Gets the funder at a specific index
* @param index the index of the funder
* @return the address of the funder
*/
function getFunder(uint256 index) public view returns (address) {
return s_funders[index];
}
/// @notice Gets the owner of the contract
function getOwner() public view returns (address) {
return i_owner;
}
/// @notice Gets the price feed
function getPriceFeed() public view returns (IAggregator) {
return s_priceFeed;
}
/// @notice Gets the decimals of the price feed
function getDecimals() public view returns (uint8) {
return s_priceFeed.decimals();
}
/// @notice Gets the description of the price feed
function getDescription() public view returns (string memory) {
return s_priceFeed.description();
}
}- 우리는 계속해서
PriceConverter.sol파일
// PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// import IAggregator từ orakl repository
import { IAggregator } from "@bisonai/orakl-contracts/src/v0.1/interfaces/IAggregator.sol";
// Declare a library named PriceConverter
library PriceConverter {
// Declare function getPrice with input as contract interface and return uint256
function getPrice(IAggregator priceFeed) internal view returns (uint256) {
// gọi function latestRoundData() trong priceFeed
(, int256 answer,,,) = priceFeed.latestRoundData();
// Returns the ETH/USD rate with 18 digits (Oracle has 8 zeros so add 10 zeros)
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
}
// 1000000000
// call it get fiatConversionRate, since it assumes something about decimals
// It wouldn't work for every aggregator
// Convert KLAY amount to USD amount
// function getConversionRate takes input ethAmount with type uint256 and interface contract, returns uint256
function getConversionRate(uint256 ethAmount, IAggregator priceFeed) internal view returns (uint256) {
// First get the eth price using getPrice and assign it to the variable ethPrice
uint256 ethPrice = getPrice(priceFeed);
// Then multiply ethPrice by the amount of ether and divide by 18 zeros
// In solidity, we should multiply before dividing because there is no float
// This calculation is ethPrice (18 digits) * ethAmount (18 digits) / 18 digits to get back 18 digits.
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// Returns the USD value of the ether amount
// the actual ETH/USD conversation rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}- Foundry의 임포트 종속성을 처리하기 위해
forge에 Bisonai/orakl 설치forge에서 Cyfrin/foundry-devops 설치추가 재매핑 및 rpc_엔드포인트 ~에 foundry.toml 파일
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
"@bisonai/orakl-contracts/src/=lib/orakl/contracts/src/",
]
ffi = true
fs_permissions = [{ access = "read", path = "./broadcast" }]
[rpc_endpoints]
baobab = "${BAOBAB_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options- 추가
.env파일 추가BAOBAB_RPC_URLBAOBAB_RPC_URLAnkr, Allthatnodes 등에서 구할 수 있습니다.
BAOBAB_RPC_URL=https://xxxxxx/xxxxx- 에서
시험이 폴더 안에 3개의 하위 폴더를 만듭니다단위,통합,모의 시험및 파일FundMeTest.t.sol,interactionsTest.t.sol,MockDataFeedAggregator.sol
.
└── tests
├── integration
│ └── interactionsTest.t.sol
├── mocks
│ └── MockDataFeedAggregator.sol
└── unit
└── FundMeTest.t.sol3개의 파일 내용을 복사하세요
FundMeTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { DeployFundMe } from "../../script/DeployFundMe.s.sol";
import { FundMe } from "../../src/FundMe.sol";
import { HelperConfig } from "../../script/HelperConfig.s.sol";
import { Test, console } from "forge-std/Test.sol";
import { StdCheats } from "forge-std/StdCheats.sol";
contract FundMeTest is StdCheats, Test {
FundMe public fundMe;
HelperConfig public helperConfig;
uint256 public constant SEND_VALUE = 0.1 ether; // just a value to make sure we are sending enough!
uint256 public constant STARTING_USER_BALANCE = 10 ether;
uint256 public constant GAS_PRICE = 1;
address public constant USER = address(1);
// uint256 public constant SEND_VALUE = 1e18;
// uint256 public constant SEND_VALUE = 1_000_000_000_000_000_000;
// uint256 public constant SEND_VALUE = 1000000000000000000;
function setUp() external {
DeployFundMe deployer = new DeployFundMe();
(fundMe, helperConfig) = deployer.run();
vm.deal(USER, STARTING_USER_BALANCE);
}
function testPriceFeedSetCorrectly() public view {
address retreivedPriceFeed = address(fundMe.getPriceFeed());
(address expectedPriceFeed) = helperConfig.activeNetworkConfig();
assertEq(retreivedPriceFeed, expectedPriceFeed);
}
function testFundFailsWithoutEnoughETH() public {
vm.expectRevert();
fundMe.fund();
}
function testFundUpdatesFundedDataStructure() public {
vm.startPrank(USER);
fundMe.fund{value: SEND_VALUE}();
vm.stopPrank();
uint256 amountFunded = fundMe.getAddressToAmountFunded(USER);
assertEq(amountFunded, SEND_VALUE);
}
function testAddsFunderToArrayOfFunders() public {
vm.startPrank(USER);
fundMe.fund{value: SEND_VALUE}();
vm.stopPrank();
address funder = fundMe.getFunder(0);
assertEq(funder, USER);
}
// https://twitter.com/PaulRBerg/status/1624763320539525121
modifier funded() {
vm.prank(USER);
fundMe.fund{value: SEND_VALUE}();
assert(address(fundMe).balance > 0);
_;
}
function testOnlyOwnerCanWithdraw() public funded {
vm.expectRevert();
fundMe.withdraw();
}
function testWithdrawFromASingleFunder() public funded {
// Arrange
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
// vm.txGasPrice(GAS_PRICE);
// uint256 gasStart = gasleft();
// // Act
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
// uint256 gasEnd = gasleft();
// uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
// Assert
uint256 endingFundMeBalance = address(fundMe).balance;
uint256 endingOwnerBalance = fundMe.getOwner().balance;
assertEq(endingFundMeBalance, 0);
assertEq(
startingFundMeBalance + startingOwnerBalance,
endingOwnerBalance // + gasUsed
);
}
// Can we do our withdraw function a cheaper way?
function testWithdrawFromMultipleFunders() public funded {
uint160 numberOfFunders = 10;
uint160 startingFunderIndex = 2;
for (uint160 i = startingFunderIndex; i < numberOfFunders + startingFunderIndex; i++) {
// we get hoax from stdcheats
// prank + deal
hoax(address(i), STARTING_USER_BALANCE);
fundMe.fund{value: SEND_VALUE}();
}
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
assert(address(fundMe).balance == 0);
assert(startingFundMeBalance + startingOwnerBalance == fundMe.getOwner().balance);
assert((numberOfFunders + 1) * SEND_VALUE == fundMe.getOwner().balance - startingOwnerBalance);
}
}MockDataFeedAggregator.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title MockV3Aggregator
* @notice Based on the FluxAggregator contract
* @notice Use this contract when you need to test
* other contract's ability to read data from an
* aggregator contract, but how the aggregator got
* its answer is unimportant
*/
contract MockDataFeedAggregator {
uint256 public constant version = 4;
uint8 public decimals;
int256 public latestAnswer;
uint256 public latestTimestamp;
uint256 public latestRound;
mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
mapping(uint256 => uint256) private getStartedAt;
constructor(uint8 _decimals, int256 _initialAnswer) {
decimals = _decimals;
updateAnswer(_initialAnswer);
}
function updateAnswer(int256 _answer) public {
latestAnswer = _answer;
latestTimestamp = block.timestamp;
latestRound++;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = block.timestamp;
getStartedAt[latestRound] = block.timestamp;
}
function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
latestRound = _roundId;
latestAnswer = _answer;
latestTimestamp = _timestamp;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = _timestamp;
getStartedAt[latestRound] = _startedAt;
}
function getRoundData(uint80 _roundId)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
}
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (
uint80(latestRound),
getAnswer[latestRound],
getStartedAt[latestRound],
getTimestamp[latestRound],
uint80(latestRound)
);
}
function description() external pure returns (string memory) {
return "v0.6/test/mock/MockV3Aggregator.sol";
}
}interactionsTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { DeployFundMe } from "../../script/DeployFundMe.s.sol";
import { FundFundMe, WithdrawFundMe } from "../../script/Interactions.s.sol";
import { FundMe } from "../../src/FundMe.sol";
import { HelperConfig } from "../../script/HelperConfig.s.sol";
import { Test, console } from "forge-std/Test.sol";
import { StdCheats } from "forge-std/StdCheats.sol";
contract InteractionsTest is StdCheats, Test {
FundMe public fundMe;
HelperConfig public helperConfig;
uint256 public constant SEND_VALUE = 0.1 ether; // just a value to make sure we are sending enough!
uint256 public constant STARTING_USER_BALANCE = 10 ether;
uint256 public constant GAS_PRICE = 1;
address public constant USER = address(1);
// uint256 public constant SEND_VALUE = 1e18;
// uint256 public constant SEND_VALUE = 1_000_000_000_000_000_000;
// uint256 public constant SEND_VALUE = 1000000000000000000;
function setUp() external {
DeployFundMe deployer = new DeployFundMe();
(fundMe, helperConfig) = deployer.run();
vm.deal(USER, STARTING_USER_BALANCE);
}
function testUserCanFundAndOwnerWithdraw() public {
FundFundMe fundFundMe = new FundFundMe();
fundFundMe.fundFundMe(address(fundMe));
WithdrawFundMe withdrawFundMe = new WithdrawFundMe();
withdrawFundMe.withdrawFundMe(address(fundMe));
assert(address(fundMe).balance == 0);
}
}- 그러면 우리는
스크립트폴더를 만들고 파일을 생성합니다DeployFundMe.s.sol,HelperConfig.s.sol그리고Interactions.s.sol
.
└── script
├── DeployFundMe.s.sol
├── HelperConfig.s.sol
└── Interactions.s.solDeployFundMe.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import { Script } from "forge-std/Script.sol";
import { HelperConfig } from "./HelperConfig.s.sol";
import { FundMe } from "../src/FundMe.sol";
contract DeployFundMe is Script {
function run() external returns (FundMe, HelperConfig) {
HelperConfig helperConfig = new HelperConfig(); // This comes with our mocks!
address priceFeed = helperConfig.activeNetworkConfig();
vm.startBroadcast();
FundMe fundMe = new FundMe(priceFeed);
vm.stopBroadcast();
return (fundMe, helperConfig);
}
}HelperConfig.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { MockDataFeedAggregator } from "../test/mocks/MockDataFeedAggregator.sol";
import { Script } from "forge-std/Script.sol";
contract HelperConfig is Script {
NetworkConfig public activeNetworkConfig;
uint8 public constant DECIMALS = 8;
int256 public constant INITIAL_PRICE = 2000e8;
struct NetworkConfig {
address priceFeed;
}
event HelperConfig__CreatedMockPriceFeed(address priceFeed);
constructor() {
if (block.chainid == 1001) {
activeNetworkConfig = getBaobabKlayConfig();
} else {
activeNetworkConfig = getOrCreateAnvilBaobabConfig();
}
}
function getBaobabKlayConfig() public pure returns (NetworkConfig memory baobabNetworkConfig) {
baobabNetworkConfig = NetworkConfig({
priceFeed: 0x33D6ee12D4ADE244100F09b280e159659fe0ACE0 // KLAY / USDT
});
}
function getOrCreateAnvilBaobabConfig() public returns (NetworkConfig memory anvilNetworkConfig) {
// Check to see if we set an active network config
if (activeNetworkConfig.priceFeed != address(0)) {
return activeNetworkConfig;
}
vm.startBroadcast();
MockDataFeedAggregator mockPriceFeed = new MockDataFeedAggregator(
DECIMALS,
INITIAL_PRICE
);
vm.stopBroadcast();
emit HelperConfig__CreatedMockPriceFeed(address(mockPriceFeed));
anvilNetworkConfig = NetworkConfig({priceFeed: address(mockPriceFeed)});
}
}Interactions.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import { Script, console } from "forge-std/Script.sol";
import { FundMe } from "../src/FundMe.sol";
import { DevOpsTools } from "foundry-devops/src/DevOpsTools.sol";
contract FundFundMe is Script {
uint256 SEND_VALUE = 0.1 ether;
function fundFundMe(address mostRecentlyDeployed) public {
vm.startBroadcast();
FundMe(payable(mostRecentlyDeployed)).fund{value: SEND_VALUE}();
vm.stopBroadcast();
console.log("Funded FundMe with %s", SEND_VALUE);
}
function run() external {
address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("FundMe", block.chainid);
fundFundMe(mostRecentlyDeployed);
}
}
contract WithdrawFundMe is Script {
function withdrawFundMe(address mostRecentlyDeployed) public {
vm.startBroadcast();
FundMe(payable(mostRecentlyDeployed)).withdraw();
vm.stopBroadcast();
console.log("Withdraw FundMe balance!");
}
function run() external {
address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("FundMe", block.chainid);
withdrawFundMe(mostRecentlyDeployed);
}
}- 배포 Baobab 테스트넷에 배포하려면 이 명령을 사용하세요
forge script script/DeployFundMe.s.sol --rpc-url $BAOBAB_RPC_URL --account $WALLET_NAME --sender $SENDER_ADDRESS --broadcast --gas-estimate-multiplier 200--가스 추정 배율 200- 가스가 부족하여 거래 오류가 발생할 수 있으므로, 가스 예상치를 2배로 곱해야 합니다--발신자 $SENDER_ADDRESS- 교체$SENDER_ADDRESS주소와 함께--계정 $WALLET_NAME- 다음 명령어를 사용하여 설정할 수 있습니다캐스트 지갑 신제품그리고캐스트 지갑 가져오기. 바꾸기$WALLET_NAME저장한 키스토어의 이름을 사용하여
5. 하드햇 펀드미
하드햇 프레임워크
5.1. 개발 환경 설정
mkdirhardhat-fundme
cd hardhat-fundmeHardhat을 사용하려면 개발 환경을 설정하고 Hardhat을 설치해야 합니다. 다음 단계에 따라 진행해 봅시다:
1단계: 프로젝트 디렉터리를 생성합니다
2단계: npm 프로젝트 초기화
터미널에 이 명령어를 붙여넣어 package.json 파일을 생성하세요
npm init -y3단계: hardhat 및 기타 종속성 설치
- 터미널에 아래 코드를 붙여넣어 hardhat을 설치하세요
npm install --save-dev hardhat- 다른 종속성을 설치하려면 아래 코드를 붙여넣으세요
npm install dotenv @bisonai/orakl-contracts4단계: hardhat 프로젝트 초기화
npx hardhat init터미널에 표시된 안내 사항을 꼭 따라주세요. 이 프로젝트에서는 hardhat 자바스크립트 프로젝트를 선택하고 hardhat-toolbox를 설치했습니다.
hardhat 프로젝트를 초기화한 후, 현재 디렉토리에는 다음 파일들이 포함되어 있어야 합니다:
1. 계약서/ – 이 폴더에는 스마트 계약 코드가 포함되어 있습니다.
2. ignition/ – 이 폴더에는 블록체인 네트워크에 계약을 배포하는 코드가 포함되어 있습니다.
3. test/ – 이 폴더에는 스마트 계약을 테스트하는 모든 단위 테스트가 포함되어 있습니다.
4. hardhat.config.js – 이 파일에는 중요한 구성 정보가 포함되어 있습니다 작동 작업 Hardhat의 Hardhat의작동, 배포 및 검증 FundMe 계약의 FundMe 계약의 배포 및 검증.5단계: .env 파일 만들기
이제 프로젝트 폴더에 .env 파일을 생성하세요. 이 파일은 환경 변수를 불러오는 데 도움이 됩니다. .env ~에 제출하다 process.env.
- 터미널에 이 명령어를 붙여넣어 .env 파일을 생성하세요
.env 파일 열기- 파일을 생성한 후, .env 파일을 아래와 같이 설정해 봅시다. Kairos RPC URL은 다음 링크에서 확인할 수 있습니다:
KAIA_KAIROS_URL= "귀하의 Kairos RPC 링크"
PRIVATE_KEY= "MetaMask 지갑에서 복사한 개인 키"6단계: Hardhat 구성 설정
[설정]을 수정하세요 hardhat.config.js 다음과 같은 구성으로:
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config()
module.exports = {
solidity: "0.8.24",
networks: {
kairos: {
url: process.env.KAIA_KAIROS_URL || "",
gasPrice: 250000000000,
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
}
},
etherscan: {
apiKey: {
kairos: "unnecessary",
},
customChains: [
{
network: "kairos",
chainId: 1001,
urls: {
apiURL: "https://api-baobab.klaytnscope.com/api",
browserURL: "https://baobab.klaytnscope.com",
},
},
]
}
};
개발 환경 설정이 모두 완료되었으니, 이제 Fundme 스마트 계약을 작성해 보겠습니다.
5.2. FundMe 스마트 계약 생성
contracts 폴더에서 다음을 생성합니다 FundMe.sol 그리고 PriceConverter.sol 각각의 파일.
FundMe.sol
// FundMe.sol
// SPDX-License-Identifier: MIT
// 1. Pragma
pragma solidity ^0.8.19;
// 2. Imports
// We import the orakl library so we can interact with oracle
import { IFeedProxy } from "@bisonai/orakl-contracts/v0.2/src/interfaces/IFeedProxy.sol";
// We import the PriceConverter library so we can calculate the KLAY value
import { PriceConverter } from "./PriceConverter.sol";
// 3. Interfaces, Libraries, Contracts
// Declaring error is not the Owner of the contract
error FundMe__NotOwner();
/**
* @title A sample Funding Contract
* @author Patrick Collins
* @notice This contract is for creating a sample funding contract
* @dev This implements price feeds as our library
*/
contract FundMe {
// Type Declarations
// The next line means
// use the PriceConverter library for variables with type uint256
using PriceConverter for uint256;
// State variables
// Declare a public constant MINIMUM_USD with value $5 but equal to peb so must multiply by 10^18
uint256 public constant MINIMUM_USD = 5 * 10 ** 18;
// Declare a private and immutable address with the name i_owner, i means immutable.
address private immutable i_owner;
// Declare a private array containing a list of people who fund ether with the name s_funders, s means storage.
address[] private s_funders;
// Declare a mapping between address and private uint256 linking the address with the fund amount.
mapping(address => uint256) private s_addressToAmountFunded;
// Declare contract AggregatorV2Interface internal and assign it to the variable s_dataFeed, s means storage
IFeedProxy internal s_dataFeed;
// Events (we have none!)
// Modifiers
// Declare an onlyOwner modifier to assign to a function that only the owner can call
modifier onlyOwner() {
// require(msg.sender == i_owner);
if (msg.sender != i_owner) revert FundMe__NotOwner();
_;
}
// Functions Order:
//// constructor
//// receive
//// fallback
//// external
//// public
//// internal
//// private
//// view / pure
// Declaring a constructor with an address for priceFeed implies that this is the address of the Oracle contract with IAggregator
constructor(address feedProxy) {
// Input the address into the interface and assign it to the variable s_priceFeed
// s_priceFeed = IAggregator(priceFeed);
s_dataFeed = IFeedProxy(feedProxy);
// Assign the variable i_owner to msg.sender (the person who deploys this contract)
i_owner = msg.sender;
}
/// @notice Funds our contract based on the KLAY/USDT price from Orakl
// Deposit to our contract based on ETH/USD price
function fund() public payable {
require(msg.value.getConversionRate(s_dataFeed) >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
// Then map the sender's address with msg.value in mapping s_addressToAmountFunded
s_addressToAmountFunded[msg.sender] += msg.value;
// Then add the sender address to the list of funders
s_funders.push(msg.sender);
}
function withdraw() public onlyOwner {
// Use for loop, starting from index 0 to index less than the length of the list, and index plus 1 for each loop
for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) {
// assign the address value at funderIndex in the s_funders list to the funder address
address funder = s_funders[funderIndex];
// Change the value of mapping s_addressToAmountFunded whose address is funder to 0, meaning this funder has withdrawn
s_addressToAmountFunded[funder] = 0;
}
// Create a new s_funders list with a new dynamic array (literally a list) of size 0
s_funders = new address[](0);
// Transfer vs call vs Send
// Transfer vs call vs Send
// - transfer (2300 gas, throws error if any)
// - send (2300 gas, returns bool for success or failure)
// - call (forward all gas or set gas, returns bool for success or failure)
// payable(msg.sender).transfer(address(this).balance);
// Send the entire balance of this contract to i_owner with no data in the transaction and return boolean success or not
(bool success,) = i_owner.call{value: address(this).balance}("");
// Require bool success true otherwise revert completely
require(success);
}
function cheaperWithdraw() public onlyOwner {
// Copy the list of s_funders from storage to memory, that is, load from global state to local state. Changing global state consumes more gas than local state
address[] memory funders = s_funders;
// mappings can't be in memory, sorry!
for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
address funder = funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
// payable(msg.sender).transfer(address(this).balance);
(bool success,) = i_owner.call{value: address(this).balance}("");
require(success);
}
/** Getter Functions */
// Functions are only used to GET information
/**
* @notice Gets the amount that an address has funded
* @param fundingAddress the address of the funder
* @return the amount funded
*/
function getAddressToAmountFunded(address fundingAddress) public view returns (uint256) {
return s_addressToAmountFunded[fundingAddress];
}
/**
* @notice Gets the funder at a specific index
* @param index the index of the funder
* @return the address of the funder
*/
function getFunder(uint256 index) public view returns (address) {
return s_funders[index];
}
/// @notice Gets the owner of the contract
function getOwner() public view returns (address) {
return i_owner;
}
/// @notice Gets the price feed
function getPriceFeed() public view returns (address) {
return s_dataFeed.getFeed();
}
/// @notice Gets the decimals of the price feed
function getDecimals() public view returns (uint8) {
return s_dataFeed.decimals();
}
}PriceConverter.sol
// PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// import IAggregator từ orakl repository
import { IFeedProxy } from "@bisonai/orakl-contracts/v0.2/src/interfaces/IFeedProxy.sol";
// Declare a library named PriceConverter
library PriceConverter {
// Declare function getPrice with input as contract interface and return uint256
function getPrice(IFeedProxy dataFeed) internal view returns (uint256) {
// gọi function latestRoundData() trong priceFeed
(, int256 answer,) = dataFeed.latestRoundData();
// Returns the ETH/USD rate with 18 digits (Oracle has 8 zeros so add 10 zeros)
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
}
// 1000000000
// call it get fiatConversionRate, since it assumes something about decimals
// It wouldn't work for every aggregator
// Convert KLAY amount to USD amount
// function getConversionRate takes input ethAmount with type uint256 and interface contract, returns uint256
function getConversionRate(uint256 ethAmount, IFeedProxy dataFeed) internal view returns (uint256) {
// First get the eth price using getPrice and assign it to the variable ethPrice
uint256 ethPrice = getPrice(dataFeed);
// Then multiply ethPrice by the amount of ether and divide by 18 zeros
// In solidity, we should multiply before dividing because there is no float
// This calculation is ethPrice (18 digits) * ethAmount (18 digits) / 18 digits to get back 18 digits.
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// Returns the USD value of the ether amount
// the actual ETH/USD conversation rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}5.3. FundMe 스마트 계약 테스트
1단계: 탐색기 창에서 테스트 폴더를 선택한 다음 ‘새 파일’ 버튼을 클릭하여 다음 이름의 새 파일을 만듭니다. Fundme.js
2단계: 만들기 MockDataFeedAggregator.sol 테스트를 위해 contract 폴더에. 아래 코드를 이 파일에 복사하여 붙여넣으세요:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title MockV3Aggregator
* @notice Based on the FluxAggregator contract
* @notice Use this contract when you need to test
* other contract's ability to read data from an
* aggregator contract, but how the aggregator got
* its answer is unimportant
*/
contract MockDataFeedAggregator {
uint256 public constant version = 4;
uint8 public decimals;
int256 public latestAnswer;
uint256 public latestTimestamp;
uint256 public latestRound;
mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
mapping(uint256 => uint256) private getStartedAt;
constructor(uint8 _decimals, int256 _initialAnswer) {
decimals = _decimals;
updateAnswer(_initialAnswer);
}
function updateAnswer(int256 _answer) public {
latestAnswer = _answer;
latestTimestamp = block.timestamp;
latestRound++;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = block.timestamp;
getStartedAt[latestRound] = block.timestamp;
}
function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
latestRound = _roundId;
latestAnswer = _answer;
latestTimestamp = _timestamp;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = _timestamp;
getStartedAt[latestRound] = _startedAt;
}
function getRoundData(uint80 _roundId)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
}
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (
uint80(latestRound),
getAnswer[latestRound],
getStartedAt[latestRound],
getTimestamp[latestRound],
uint80(latestRound)
);
}
function description() external pure returns (string memory) {
return "v0.6/test/mock/MockV3Aggregator.sol";
}
}3단계: 아래 코드를 복사하여 Fundme.js 파일:
// Fundme.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
describe("FundMe", function () {
async function deployContractsFixture() {
const [deployer, addr1, addr2] = await ethers.getSigners();
const MockDataFeedAggregator = await ethers.getContractFactory("MockDataFeedAggregator");
const mockPriceFeed = await MockDataFeedAggregator.connect(deployer).deploy(8, 2000 * 10 ** 8); // Example price of 2000 USD with 8 decimals
await mockPriceFeed.waitForDeployment(); // Ensure the contract is deployed
// Use fully qualified name for FundMe contract
const FundMe = await ethers.getContractFactory("contracts/FundMe.sol:FundMe");
const fundMe = await FundMe.connect(deployer).deploy(mockPriceFeed.target);
await fundMe.waitForDeployment(); // Ensure the contract is deployed
return { fundMe, mockPriceFeed, deployer, addr1, addr2 };
}
describe("Deployment", function () {
it("Should set the right owner", async function () {
const { fundMe, deployer } = await loadFixture(deployContractsFixture);
expect(await fundMe.getOwner()).to.equal(deployer.address);
});
});
describe("Fund", function () {
it("Should accept funds", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("1"); // 1 ETH
await fundMe.connect(addr1).fund({ value: sendValue });
expect(await fundMe.getAddressToAmountFunded(addr1.address)).to.equal(sendValue);
});
it("Should require a minimum amount in USD", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("0.001"); // 0.001 ETH, less than minimum
await expect(fundMe.connect(addr1).fund({ value: sendValue })).to.be.revertedWith(
"You need to spend more ETH!"
);
});
});
describe("Withdraw", function () {
it("Should withdraw ETH correctly", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("1"); // 1 ETH
await fundMe.connect(addr1).fund({ value: sendValue });
await fundMe.withdraw();
expect(await ethers.provider.getBalance(fundMe.target)).to.equal(0);
});
it("Should only allow the owner to withdraw", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("1"); // 1 ETH
await fundMe.connect(addr1).fund({ value: sendValue });
await expect(fundMe.connect(addr1).withdraw()).to.be.revertedWithCustomError(
fundMe,
"FundMe__NotOwner"
);
});
});
});
4단계: 테스트를 실행하려면 터미널을 열고 아래 명령어를 실행하세요:
5.4. 스마트 계약 배포
1단계: 탐색 창에서 다음을 선택합니다. 점화 장치/모듈 폴더로 이동한 다음 ‘새 파일’ 버튼을 클릭하여 Fundme.js
2단계: 다음 코드를 파일 안에 복사하여 붙여넣으세요.
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const PRICE_FEED_ADDRESS = "0x1408cb13d84ba8cb533fdf332db5d78290b071c9";
module.exports = buildModule("FundMeModule", (m) => {
const priceFeedAddr = m.getParameter("_priceFeed", PRICE_FEED_ADDRESS);
const fundMe = m.contract("FundMe", [priceFeedAddr], {});
return { fundMe };
});3단계: 터미널에서 다음 명령어를 실행하여 Hardhat이 Fundme 계약을 Kaia 테스트넷(Kairos)에 배포하도록 지시합니다.
npx hardhat ignition deploy ignition/modules/Fundme.js --network kairos5.5. 스마트 계약 검증
터미널에 아래 코드를 복사하여 붙여넣으세요:
// example
// npx hardhat verify –network <network> <deployed_address> <parameters>
npx hardhat verify --network kairos 0xa9a6f38b67d7ba2b716d0d2bd21974e2149df7ef 0xf0d6Ccdd18B8A7108b901af872021109C27095bA