1. Remix IDE Tests

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. Xây dựng Test

Việc test kỹ lưỡng các file Solidity đóng vai trò quan trọng trong việc đảm bảo chất lượng, độ tin cậy và bảo mật của dự án blockchain của bạn.

Dưới đây là một số lý do chính:

  • Phát hiện lỗi: Việc test giúp bạn xác định và sửa lỗi trong code Solidity của bạn. Lỗi code có thể dẫn đến các hành vi không mong muốn, thậm chí là mất mát tài sản. Việc test giúp bạn phát hiện những lỗi này sớm và sửa chữa chúng trước khi chúng gây ra vấn đề nghiêm trọng.
  • Đảm bảo tính bảo mật: Việc test giúp bạn xác định các lỗ hổng bảo mật trong code Solidity của bạn. Lỗ hổng bảo mật có thể khiến dự án của bạn dễ bị tấn công bởi hacker. Việc test giúp bạn xác định những lỗ hổng này và thực hiện các biện pháp để vá chúng.
  • Tăng độ tin cậy: Việc test giúp bạn nâng cao độ tin cậy của dự án blockchain của bạn. Khi người dùng biết rằng dự án của bạn đã được test kỹ lưỡng, họ sẽ tin tưởng hơn vào dự án và có nhiều khả năng sử dụng nó hơn.
  • Tiết kiệm thời gian và tiền bạc: Việc test giúp bạn tiết kiệm thời gian và tiền bạc trong dài hạn. Việc sửa lỗi sớm hơn sẽ giúp bạn tránh được những vấn đề nghiêm trọng hơn và tốn kém hơn về sau.

Có nhiều phương pháp test khác nhau có thể được sử dụng để test file Solidity. Một số phương pháp phổ biến bao gồm:

  1. Unit test: là phương pháp test từng đơn vị code riêng lẻ.
  2. Integration test: là phương pháp test cách các đơn vị code khác nhau hoạt động cùng nhau.
  3. Forked test: là phương pháp test code trong môi trường giả lập môi trường thật.
  4. Staging test: là phương pháp test code trong môi trường thật nhưng không phải prod. Việc lựa chọn phương pháp test phù hợp sẽ phụ thuộc vào nhu cầu cụ thể của dự án bạn.

Dưới đây là một số lời khuyên để test file Solidity hiệu quả:

  • Viết code test dễ hiểu và dễ bảo trì.
  • Sử dụng nhiều phương pháp test khác nhau.
  • Tự động hóa việc test.
  • Xem xét sử dụng các công cụ test chuyên dụng.

Viết test thế nào

Remix, hoặc Hardhat, hoặc Foundry có những phương án riêng cho việc test smart contract. Bạn có thể tham khảo sâu docs ở dưới đây:

3. Tìm hiểu thêm về Contract của Solidity

3.1. constructor

Constructor là function chạy ngay lập tức khi smart contract được khởi tạo

// 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. State variable visibility

  • public - biến public giống biến internal (cho phép contract hiện tại và các contract thừa hưởng truy cập) nhưng sẽ tự động tạo ra getter function để các contract ngoài có thể truy cập luôn.
  • internal - biến chỉ có thể được truy cập bởi contract hiện tại và các contract thừa hưởng. Đây cũng là default visibility cho state variable.
  • private - biến chỉ có thể được truy cập bởi contract hiện tại.
Note:
Biến internalprivate chỉ hạn chế truy cập tới contract khác. Giá trị của biến vẫn có thể được thấy bởi tất cả mọi người.

3.3. Function visibility

  • external - function chỉ có thể được gọi từ ngoài vào.
  • public - function vừa có thể được gọi bằng function khác trong contract, vừa có thể được gọi từ ngoài vào.
  • internal - function chỉ có thể được gọi bởi contract hiện tại hoặc contract thừa hưởng.
  • private - function chỉ có thể được gọi bởi contract hiện tại.

3.4. Getter function

function dùng để gọi vào biến public mà compiler tự động tạo. Cũng được sử dụng để chỉ khái niệm function dùng để query biến để xem.

// 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. Constants and immutable state variables

  • constant - biến có giá trị được cố định ngay khi compile (cho vào contract bytecode).
  • immutable - biến có giá trị có thể được gán vào trong thời gian construct.
// 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. Pure function

function không đọc hoặc thay đổi state của blockchain. Hay được sử dụng làm function tính toán.

// 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. Payable functions and addresses

// 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. Receive Ether and Fallback function

Một contract có thể có tối đa một receive function, được khai báo bằng cách sử dụng receive() external payable { ... } (không có từ khóa function). function này không được có argument, không được return bất kỳ thứ gì và phải có external visibility cũng như payablestate mutability. Nó có thể là virtual, có thể override và có thể có các modifier.

    Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()
// 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. Oracle

Oracle cho smart contract là cầu nối giữa blockchain và thế giới bên ngoài. Nó cung cấp dữ liệu cho smart contract từ các nguồn bên ngoài blockchain, chẳng hạn như API, dữ liệu thị trường, dữ liệu thời tiết, v.v...

Dưới đây là một số ví dụ về cách sử dụng oracle cho smart contract:

  • Cung cấp dữ liệu giá cho thị trường phi tập trung (DeFi): Oracle có thể cung cấp dữ liệu giá cho các tài sản tiền mã hóa, cho phép các nhà giao dịch thực hiện giao dịch trên các sàn giao dịch phi tập trung.
  • Kích hoạt hợp đồng bảo hiểm: Oracle có thể cung cấp dữ liệu về các sự kiện bảo hiểm, chẳng hạn như tai nạn hoặc thiên tai, để kích hoạt thanh toán bảo hiểm.
  • Tự động hóa các quy trình: Oracle có thể được sử dụng để tự động hóa các quy trình, chẳng hạn như thanh toán hóa đơn hoặc quản lý chuỗi cung ứng.

Danh sách các Oracle trên Kaia: https://klaytn.foundation/ecosystem/?search=&cate=oracles-bridges&sort=abc

4. Foundry Fundme

4.1. Framework Foundry

Trên thực tế, Remix IDE có nhiều giới hạn về mặt tính năng nên chúng ta sẽ sử dụng Foundry, một framework để phát triển, test và deploy smart contract.

4.2. Cài đặt

Truy cập website GetFoundry.sh và đi theo hướng dẫn.

4.3. Bắt đầu

Truy cập và làm theo hướng dẫn trong Foundry Book để khởi tạo project.

4.4. Fund Me project

Bài tập này dựa trên repo Foundry FundMe của Patrick Collins những đã được cập nhật để phù hợp với môi trường của Klaytn.

  1. Đầu tiên chạy forge init klaytn-fund-me
  2. Sau đó chúng ta sẽ tạo file 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. Chúng ta tiếp tục tạo file 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;
    }
}
  1. Để xử lý phần import dependencies cho Foundry
forge install Bisonai/orakl
forge install Cyfrin/foundry-devops

Add remappings and rpc_endpoints to foundry.toml file

[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. Thêm file .env và thêm vào BAOBAB_RPC_URL BAOBAB_RPC_URL có thể lấy được trên Ankr, Allthatnodes, v.v...
BAOBAB_RPC_URL=https://xxxxxx/xxxxx
  1. Trong thư mục tests chúng ta tạo 3 thư mục con unit, integration, mocks và các file FundMeTest.t.sol, interactionsTest.t.sol, MockDataFeedAggregator.sol
.
└── tests
    ├── integration
    │   └── interactionsTest.t.sol
    ├── mocks
    │   └── MockDataFeedAggregator.sol
    └── unit
        └── FundMeTest.t.sol

Copy nội dung của 3 file vào

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);
    }
}
  1. Then we go to the scripts folder and create the files DeployFundMe.s.sol, HelperConfig.s.sol and Interactions.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);
    }
}

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);
    }
}
  1. Deploy Sử dụng lệnh này để deploy lên testnet Baobab
forge script script/DeployFundMe.s.sol --rpc-url $BAOBAB_RPC_URL --account $WALLET_NAME --sender $SENDER_ADDRESS --broadcast --gas-estimate-multiplier 200
  • --gas-estimate-multiplier 200 - is to multiply the gas estimate by 2 because there may be transaction errors due to not enough gas
  • --sender $SENDER_ADDRESS - Replace $SENDER_ADDRESS with your address
  • --account $WALLET_NAME - You can set it up with the commands cast wallet new and cast wallet import. Replace $WALLET_NAME with the name of the keystore you saved

5. Hardhat Fundme

Ngoài foundry, chúng ta còn có 1 framework nữa là hardhat framework, một framework để phát triển, test và deploy smart contract.

Cài đặt

Để sử dụng hardhat, chúng ta cần thiết lập môi trường phát triển và cài đặt hardhat. Hãy thực hiện việc này theo các bước sau:

5.1. Tạo thư mục cho project của bạn

mkdir hardhat-fundme
cd hardhat-fundme

5.2: Khởi tạo node_module

Dán lệnh này vào terminal của bạn để tạo tệp package.json

npm init -y

5.3: Cài đặt hardhat và các dependencies khác

  • Dán mã dưới đây vào terminal của bạn để cài đặt hardhat
npm install --save-dev hardhat
  • Dán mã dưới đây để cài đặt các dependencies khác
npm install dotenv @bisonai/orakl-contracts

5.4: Khởi tạo một dự án hardhat

npx hardhat init

Hãy chắc chắn làm theo các hướng dẫn được hiển thị trong terminal của bạn. Đối với dự án này, chúng tôi đã chọn hardhat javascript project và cài đặt hardhat-toolbox.

Sau khi khởi tạo dự án hardhat, thư mục hiện tại của bạn sẽ bao gồm:

1. contracts/ – this folder contains smart contract code.

2. ignition/ – this folder contains code that deploys your contracts on the blockchain network.

3. test/ – this folder contains all unit tests that test your smart contract.

4. hardhat.config.js – this file contains configurations important for the work of Hardhat, deployment and verification of the FundMe contract.

5.5: Tạo một file .env

Tạo một file .env trong project folder của bạn để chứa những biến môi trường.

  • Dán lệnh này vào terminal của bạn để tạo file .env
touch .env
  • Sau khi tạo file, giờ chúng ta thêm nội dung vào file như vầy:
KAIA_KAIROS_URL= "Your Kairos RPC link"
 PRIVATE_KEY= "your private key copied from MetaMask wallet"

5.6: Điều chỉnh Hardhat config của bạn

Thay đổi file hardhat.config.js với những tuỳ chỉnh sau:

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

Sau khi chúng ta đã thiết lập xong môi trường Hardhat, hãy bắt đầu viết smart contract FundMe.

Tạo FundMe Smart Contract

Trong thư mục contracts, bạn sẽ tạo tệp FundMe.solPriceConverter.sol tương ứng.

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

Testing FundMe Smart Contract

Step 1: Chọn thư mục test và tạo một tệp mới có tên là Fundme.js

Step 2: Tạo một tệp MockDataFeedAggregator.sol trong thư mục contracts cho mục đích kiểm tra. Sao chép và dán mã dưới đây vào tệp này:

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

Bước 3: Sao chép và dán mã dưới đây vào tệp 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"
      );
    });
  });
});

Bước 4: Để chạy test, mở terminal và chạy command ở dưới

npx hardhat test test/Fundme.js

Deploy smart contract lên blockchain

Bước 1: Chọn thư mục Ignition/modules và tạo một tệp mới có tên là Fundme.js

Bước 2: Sao chép và dán code dưới đây vào file của bạn:

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

Bước 3: Trong terminal, chạy lệnh sau để yêu cầu Hardhat triển khai hợp đồng Fundme của bạn trên Kaia Testnet (Kairos)

npx hardhat ignition deploy ignition/modules/Fundme.js --network kairos

Xác minh smart contract

Sao chép và dán code dưới đây vào terminal của bạn:

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

npx hardhat verify --network kairos 0xa9a6f38b67d7ba2b716d0d2bd21974e2149df7ef 0xf0d6Ccdd18B8A7108b901af872021109C27095bA