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ファイルの徹底的なテストは、ブロックチェーンプロジェクトの品質、信頼性、およびセキュリティを確保する上で重要な役割を果たします。

主な理由をいくつか挙げます:

  • エラー検出: テストはSolidityコード内のエラーを特定し修正するのに役立ちます。コードエラーは意図しない動作や資産の損失さえ引き起こす可能性があります。テストはこれらのエラーを早期に検出し、深刻な問題を引き起こす前に修正するのに役立ちます。
  • セキュリティの確保:テストはSolidityコードのセキュリティ脆弱性を特定するのに役立ちます。セキュリティ脆弱性はプロジェクトをハッカーの攻撃に晒す可能性があります。テストによりこれらの脆弱性を特定し、修正措置を講じることができます。
  • 信頼性の向上:テストはブロックチェーンプロジェクトの信頼性向上に役立ちます。ユーザーがプロジェクトが徹底的にテストされていることを知れば、プロジェクトへの信頼が高まり、利用する可能性も増えます。
  • 時間と費用を節約:テストは長期的に見て時間と費用を節約します。エラーを早期に修正することで、後々より深刻で費用のかかる問題を回避できます。

Solidityファイルをテストするために使用できる様々なテスト手法があります。代表的な手法には以下のようなものがあります:

  1. ユニットテスト:コードの各個別の単位をテストする方法である。
  2. 統合テスト:異なるコードユニットがどのように連携して動作するかをテストする方法である。
  3. 分岐テスト:実際の環境をシミュレートした環境でコードをテストする方法である。
  4. ステージングテスト:本番環境ではないが実際の環境でコードをテストする方法です。適切なテスト手法の選択は、プロジェクトの具体的な要件によって異なります。

Solidityファイルを効果的にテストするためのヒントをいくつか紹介します:

  • 理解しやすく保守しやすいテストコードを書く。
  • 様々なテスト手法を活用する。
  • テストを自動化する。
  • 専用のテストツールの使用を検討してください。

テスト方法

Remix、Hardhat、Foundryにはそれぞれ独自のスマートコントラクトテスト機能があります。詳細は以下の詳細なドキュメントを参照してください:

3. Solidityにおける契約に関する詳細情報

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. 関数の可視性

  • 外部 - 関数 外部からのみ呼び出せるもの。
  • 公開 - 関数 どちらも別のものによって呼び出されることができる 関数 in 契約また外部からも呼び出すことができます。
  • 内部 - 関数 既存の 契約 または継承された 契約.
  • 非公開 - 関数 現在の 契約.

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()外部ペイアブル { ... } (なし) 関数 キーワード). この 関数 あってはならない 議論できない 戻る 何でもあり、そして必ず持っていなければならない 外部 可視性だけでなく 支払うべき 状態の可変性それは~である可能性がある 仮想それは~である可能性がある オーバーライド そしてそれは持つことができる 修飾子.

    どちら 関数  呼び出されるフォールバック() または 受信()?

           送信 イーサ
               |
         msg.データ  ですか?
              / \
            はい  いいえ
            /     \
受信() 存在する?  フォールバック()
         /   \
        はい   いいえ
        /      \
    受信()   フォールバック()
// 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、市場データ、気象データなど、ブロックチェーン外のソースからスマートコントラクトにデータを提供します。

スマートコントラクトにおけるOracleの使用例をいくつかご紹介します:

  • 分散型市場(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. Fund Meプロジェクト

この演習は、パトリック・コリンズのFoundry FundMeリポジトリに基づいていますが、Klaytn の環境に合わせて更新されています。

  1. 初回 forge init klaytn-fund-me
  2. 次に作成します 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();
    }
}
  1. 私たちは創造を続けています 価格変換器.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;
    }
}
  1. Foundryのインポート依存関係を処理するため
forge install Bisonai/orakl
forge install Cyfrin/foundry-devops

追加 再マッピングと RPCエンドポイント ~へ 鋳造所設定ファイル

[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
  1. 追加 .env ファイルを追加する バオバブ_RPC_URL バオバブ_RPC_URL Ankr、Allthatnodesなどで入手可能です。
BAOBAB_RPC_URL=https://xxxxxx/xxxxx
  1. において テスト 作成するフォルダ内に3つのサブフォルダを作成します ユニット, 統合, 嘲笑 およびファイル FundMeTest.t.sol, インタラクションテスト.t.sol, MockDataFeedAggregator.sol
.
└── tests
    ├── integration
    │   └── interactionsTest.t.sol
    ├── mocks
    │   └── MockDataFeedAggregator.sol
    └── unit
        └── FundMeTest.t.sol

3つのファイルの内容をコピーしてください

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";
    }
}

インタラクションテスト.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);
    }
}
  1. それから私たちは スクリプト フォルダを作成し、ファイルを作成する DeployFundMe.s.sol, ヘルパー設定.s.sol そして 相互作用.s.sol
└── script
    ├── DeployFundMe.s.sol
    ├── HelperConfig.s.sol
    └── Interactions.s.sol

DeployFundMe.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);
    }
}

ヘルパー設定.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)});
    }
}

相互作用.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);
    }
}
  1. デプロイこのコマンドを使用して、バオバブテストネットにデプロイします。
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-fundme

ハードハットを利用するには、開発環境をセットアップし、ハードハットをインストールする必要があります。以下の手順で進めましょう:

ステップ1: プロジェクトディレクトリを作成する

ステップ2: npmプロジェクトを初期化する

このコマンドをターミナルに貼り付けて、package.json ファイルを作成してください。

npm init -y

ステップ3: hardhatおよびその他の依存関係をインストールする

  • 以下のコードをターミナルに貼り付けてハードハットをインストールしてください
npm install --save-dev hardhat
  • 以下のコードを貼り付けて、他の依存関係をインストールしてください
npm install dotenv @bisonai/orakl-contracts

ステップ4: ハードハットプロジェクトの初期化

npx hardhat init

ターミナルでハイライト表示されたプロンプトに従ってください。このプロジェクトでは、hardhat javascript projectを選択し、hardhat-toolbox をインストールしました。

ハードハットプロジェクトを初期化した後、現在のディレクトリには以下が含まれるはずです:

1. 契約書/ – この フォルダにはスマートコントラクトのコードが含まれています。

2. ignition/ – この フォルダには、ブロックチェーンネットワーク上に契約を展開するコードが含まれています。

3. test/ – この フォルダには、スマートコントラクトをテストするすべてのユニットテストが含まれています。

4. hardhat.config.js – この ファイルには、  作業  デプロイおよび検証  FundMe契約の

ステップ5:.envファイルを作成する

プロジェクトフォルダ内に.envファイルを作成してください。このファイルは環境変数を .env ファイルに process.env.

  • このコマンドをターミナルに貼り付けて、.envファイルを作成してください
.env を編集する
  • ファイルを作成したら、.envファイルを以下のように設定しましょう。Kairos RPCのURLはここで確認できます
KAIA_KAIROS_URL= "あなたのカイロスRPCリンク"
 PRIVATE_KEY= "MetaMaskウォレットからコピーした秘密鍵"

ステップ6: ハードハット設定のセットアップ

変更してください ハードハット設定ファイル 以下の構成で:

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スマートコントラクトの作成

契約書フォルダ内で、作成します FundMe.sol そして 価格変換器.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();
    }
}

価格変換器.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 テスト目的で契約フォルダ内に配置してください。以下のコードをこのファイルにコピー&ペーストしてください:

// 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 kairos

5.5. スマートコントラクトの検証

以下のコードをターミナルにコピーして貼り付けてください:

// example
// npx hardhat verify –network <network> <deployed_address> <parameters>

npx hardhat verify --network kairos 0xa9a6f38b67d7ba2b716d0d2bd21974e2149df7ef 0xf0d6Ccdd18B8A7108b901af872021109C27095bA