1. Kiểm thử 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. Kiểm thử các tệp Solidity

Việc kiểm tra kỹ lưỡng các tập tin Solidity đóng vai trò quan trọng trong việc đảm bảo chất lượng, độ tin cậy và tính 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: Kiểm thử giúp bạn xác định và sửa lỗi trong mã Solidity của mình. Lỗi mã có thể dẫn đến hành vi không mong muốn và thậm chí mất dữ liệu. Kiểm thử giúp bạn phát hiện những lỗi này sớm và sửa chúng trước khi chúng gây ra các vấn đề nghiêm trọng.
  • Đảm bảo an ninh: Kiểm thử giúp bạn xác định các lỗ hổng bảo mật trong mã Solidity của mình. Các lỗ hổng bảo mật có thể khiến dự án của bạn dễ bị tin tặc tấn công. Kiểm thử 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 thử nghiệm giúp tăng độ tin cậy cho dự án blockchain của bạn. Khi người dùng biết rằng dự án của bạn đã được thử nghiệm 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 kiểm thử giúp bạn tiết kiệm thời gian và tiền bạc về lâu dài. Khắc phục lỗi sớm sẽ giúp bạn tránh được những vấn đề nghiêm trọng và tốn kém hơn sau này.

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

  1. Kiểm thử đơn vị: là phương pháp kiểm thử từng đơn vị mã riêng lẻ.
  2. Kiểm thử tích hợp: là một phương pháp kiểm tra cách các đơn vị mã khác nhau hoạt động cùng nhau.
  3. Kiểm thử phân nhánh (Forked test): là một phương pháp kiểm thử mã trong môi trường mô phỏng môi trường thực tế.
  4. Kiểm thử dàn dựng (staging test): là phương pháp kiểm thử mã nguồn trong môi trường thực tế chứ không phải môi trường sản xuất. Việc lựa chọn phương pháp kiểm thử phù hợp sẽ phụ thuộc vào nhu cầu cụ thể của dự án.

Dưới đây là một số mẹo để kiểm thử hiệu quả các tập tin Solidity:

  • Hãy viết mã kiểm thử dễ hiểu và dễ bảo trì.
  • Hãy sử dụng nhiều phương pháp thử nghiệm khác nhau.
  • Tự động hóa quá trình kiểm thử.
  • Hãy cân nhắc sử dụng các công cụ kiểm thử chuyên dụng.

Làm thế nào để kiểm tra?

Remix, Hardhat hoặc Foundry đều có các tùy chọn riêng để kiểm thử hợp đồng thông minh. Bạn có thể tham khảo tài liệu chi tiết bên dưới:

3. Thông tin chi tiết hơn về Hợp đồng trong Solidity

3.1. Hàm tạo

Hàm khởi tạo (Constructor) là một hàm được chạy ngay lập tức khi hợp đồng thông minh đượ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. Khả năng hiển thị của biến trạng thái

  • công cộng - Các biến công khai tương tự như nội bộ các biến (cho phép hợp đồng hiện tại và các hợp đồng kế thừa truy cập) nhưng sẽ tự động tạo ra một hàm getter để các hợp đồng bên ngoài cũng có thể truy cập được.
  • nội bộ - Biến này chỉ có thể được truy cập bởi hợp đồng hiện tại và các hợp đồng kế thừa. Đây cũng là phạm vi hiển thị mặc định cho biến trạng thái.
  • riêng tư - Biến này chỉ có thể được truy cập bởi hợp đồng hiện tại.
Ghi chú:
Cái nội bộriêng tư Các biến chỉ hạn chế quyền truy cập vào các biến khác. hợp đồngGiá trị của biến vẫn hiển thị cho mọi người.

3.3. Khả năng hiển thị chức năng

  • bên ngoài - chức năng Chỉ có thể gọi từ bên ngoài.
  • công cộng - chức năng cả hai đều có thể được gọi bởi người khác chức năng TRONG hợp đồngvà cũng có thể được gọi từ bên ngoài.
  • nội bộ - chức năng chỉ có thể được gọi bởi một đối tượng hiện có. hợp đồng hoặc được thừa hưởng hợp đồng.
  • riêng tư - chức năng chỉ có thể được gọi bởi người dùng hiện tại hợp đồng.

3.4. Hàm getter

chức năng được dùng để gọi công cộng Biến do trình biên dịch tự động tạo ra. Cũng được dùng để chỉ khái niệm về chức năng Được sử dụng để truy vấn các 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. Hằng số và biến trạng thái bất biến

  • không thay đổi - Các biến có giá trị được cố định ngay lập tức khi biên dịch (được đưa vào mã bytecode của hợp đồng).
  • bất biến - các biến có giá trị có thể được gán trong quá trình xây dựng.
// 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. Hàm thuần túy

chức năng Không đọc hoặc thay đổi trạng thái của chuỗi khối. Hoặc được sử dụng như một phép tính. chức năng.

// 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. Chức năng và địa chỉ thanh toán

// 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. Chức năng nhận Ether và dự phòng

MỘT hợp đồng có thể có tối đa một nhận được hàm, được khai báo bằng cách sử dụng nhận() khoản phải trả bên ngoài { ... } (không có chức năng từ khóa). Cái này chức năng không được có lập luậnkhông thể trở lại bất cứ thứ gì và phải có bên ngoài khả năng hiển thị cũng như phải trả khả năng thay đổi trạng tháiNó có thể là ảonó có thể là ghi đè và nó có thể có các yếu tố bổ trợ.

Chức năng nào  được gọi là , dự phòng ( ) hoặc nhận được ( )?
           gửi Ether
|         dữ liệu tin nhắn  trống ? / \            Đúng  KHÔNG
/ \ nhận ( ) tồn tại ? dự phòng ( ) / \        Đúng   KHÔNG
/ \    nhận ( ) dự phòng ( )
// 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 dành cho hợp đồng thông minh đóng vai trò là cầu nối giữa blockchain và thế giới bên ngoài. Nó cung cấp dữ liệu cho các hợp đồng thông minh 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 hợp đồng thông minh:

  • 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 điện tử, 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 việc thanh toán bảo hiểm.
  • Tự động hóa 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 Klaytn: https://klaytn.foundation/ecosystem/?search=&cate=oracles-bridges&sort=abc

4. Quỹ gây quỹ Foundry Fundme

4.1. Framework Foundry

Trên thực tế, Remix IDE có nhiều hạn chế về tính năng, vì vậy chúng ta sẽ sử dụng Foundry, một framework để phát triển, kiểm thử và triển khai hợp đồng thông minh.

4.2. Cài đặt

Hãy truy cập trang web GetFoundry.sh và làm 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 dự án.

4.4. Dự án Fund Me

Bài tập này dựa trên kho lưu trữ 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. Lần chạy đầu tiên forge init klaytn-fund-me
  2. Sau đó chúng ta sẽ tạo ra FundMe.sol tài liệu
// 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 tôi tiếp tục tạo ra PriceConverter.sol tài liệu
// 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ý các phụ thuộc nhập khẩu cho Foundry
forge install Bisonai/orakl
giả mạo cài đặt Cyfrin/foundry-devops

Thêm vào ánh xạ lại và điểm cuối rpc ĐẾN tệp foundry.toml

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
  "@bisonai/orakl-contracts/src/=lib/orakl/contracts/src/",
]
ffi = true
fs_permissions = [{ access = "read", path = "./broadcast" }]

[rpc_endpoints]
baobab = "${BAOBAB_RPC_URL}"

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
  1. Thêm vào .env tập tin và thêm BAOBAB_RPC_URL BAOBAB_RPC_URL Có thể tìm thấy trên Ankr, Allthatnodes, v.v.
BAOBAB_RPC_URL=https: //xxxxxx/xxxxx
  1. Trong kiểm tra Thư mục này tạo ra 3 thư mục con. đơn vị, tích hợp, chế nhạo và các tệp FundMeTest.t.sol, interactionsTest.t.sol, MockDataFeedAggregator.sol
. └── kiểm thử ├── tích hợp │ └── interactionsTest.t.sol ├── mô phỏng │ └── MockDataFeedAggregator.sol └── đơn vị └── FundMeTest.t.sol

Sao chép nội dung của 3 tệp tin.

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. Sau đó chúng ta đi đến kịch bản thư mục và tạo các tệp DeployFundMe.s.sol, HelperConfig.s.solInteractions.s.sol
. └── tập lệnh ├── 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. Triển khai Sử dụng lệnh này để triển khai lên mạng thử nghiệm 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 - Nhân lượng gas ước tính với 2 vì có thể xảy ra lỗi giao dịch do không đủ gas.
  • --người gửi $SENDER_ADDRESS - Thay thế $SENDER_ADDRESS kèm theo địa chỉ của bạn
  • --tài khoản $WALLET_NAME - Bạn có thể thiết lập nó bằng các lệnh. ví đúc mớinhập khẩu ví đúc. Thay thế $WALLET_NAME với tên của kho khóa mà bạn đã lưu.

5. Quỹ gây quỹ cho mũ bảo hiểm

Khung Hardhat

5.1. Thiết lập môi trường phát triển

mkdir hardhat-fundme cd hardhat-fundme

Để 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 theo các bước sau:

Bước 1: Tạo thư mục dự án

Bước 2: Khởi tạo dự án npm

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

npm init -y

Bước 3: Cài đặt hardhat và các phần phụ thuộc khác

  • Dán đoạn mã bên dưới vào cửa sổ terminal để cài đặt Hardhat.
cài đặt npm --save-dev hardhat
  • Dán đoạn mã bên dưới để cài đặt các thư viện phụ thuộc khác.
npm install dotenv @bisonai/orakl-contracts

Bước 4: Khởi tạo dự án hardhat

npx hardhat init

Hãy chắc chắn làm theo các hướng dẫn được tô sáng trong cửa sổ terminal của bạn. Đối với dự án này, chúng tôi đã chọn dự án JavaScript Hardhat và cài đặt hardhat-toolbox .

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

1. contracts/ – thư mục này chứa mã hợp đồng thông minh. 2. ignition/ – thư mục này chứa mã lập trình để triển khai các hợp đồng của bạn trên mạng blockchain. 3. test/ – thư mục này chứa tất cả các bài kiểm thử đơn vị dùng để kiểm tra hợp đồng thông minh của bạn. 4. hardhat.config.js – tập tin này chứa các cấu hình quan trọng cho hoạt động của Hardhat, việc triển khai và xác minh hợp đồng FundMe.

Bước 5: Tạo tệp .env

Bây giờ hãy tạo tệp .env trong thư mục dự án. Tệp này giúp chúng ta tải các biến môi trường từ... .env tệp vào quá trình.env.

  • Dán lệnh này vào cửa sổ terminal để tạo tệp .env.
chạm vào .env
  • Sau khi tạo tệp, hãy cấu hình tệp .env của chúng ta sao cho giống như bên dưới. URL RPC của Kairos có thể được tìm thấy ở đây :
KAIA_KAIROS_URL= "Liên kết RPC Kairos của bạn"
PRIVATE_KEY= "khóa riêng tư của bạn được sao chép từ ví MetaMask"

Bước 6: Thiết lập cấu hình Hardhat

Hãy sửa đổi của bạn hardhat.config.js với các cấu hì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",
        },
      },
    ]
  }
};

Giờ thì môi trường phát triển đã được thiết lập xong, chúng ta hãy bắt đầu viết hợp đồng thông minh Fundme.


5.2. Tạo Hợp đồng Thông minh FundMe

Trong thư mục hợp đồng, bạn sẽ tạo FundMe.solPriceConverter.sol tệp tin 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;
    }
}

5.3. Kiểm thử hợp đồng thông minh FundMe

Bước 1: Trong ngăn Explorer, chọn thư mục test và nhấp vào nút New File để tạo một tệp mới có tên Fundme.js

Bước 2: Tạo một MockDataFeedAggregator.sol Trong thư mục hợp đồng để phục vụ mục đích thử nghiệm. Sao chép và dán đoạn mã bên dưới 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 3Sao chép và dán đoạn mã bên dưới vào Fundme.js tài liệu:

// 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 thử nghiệm, hãy mở cửa sổ dòng lệnh (terminal) và chạy lệnh sau:

5.4. Triển khai hợp đồng thông minh

Bước 1Trong ngăn Explorer, chọn Hệ thống đánh lửa/mô-đun Chọn thư mục và nhấp vào nút "Tạo tệp mới" để tạo một tệp mới có tên. Fundme.js

Bước 2: Sao chép và dán đoạn mã sau vào bên trong tệp.

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 cửa sổ dòng lệnh, chạy lệnh sau để Hardhat triển khai hợp đồng Fundme của bạn trên mạng thử nghiệm Kaia (Kairos).

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

5.5. Xác minh hợp đồng thông minh

Sao chép và dán đoạn mã bên dưới vào cửa sổ dòng lệnh của bạn:

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

npx hardhat verify --network kairos 0xa9a6f38b67d7ba2b716d0d2bd21974e2149df7ef 0xf0d6Ccdd18B8A7108b901af872021109C27095bA