1. Remix IDE 测试
1.1. 高级存储测试脚本.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. 高级存储测试.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
import {AdvancedStorage, CustomerIdentityCard} from "../AdvancedStorage.sol";
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract testSuite is AdvancedStorage {
AdvancedStorage advancedStorage;
address acc0;
address acc1;
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
advancedStorage = new AdvancedStorage();
acc0 = TestsAccounts.getAccount(0);
acc1 = TestsAccounts.getAccount(1);
}
function checkVaultManager() public returns (bool) {
return Assert.equal(this.vaultManager(), msg.sender, "Vault Manager is not correct");
}
function checkSettingInitialInvestment() public returns (bool, bool, bool) {
setInitialInvestmentVault(
10,
5,
acc1
);
return (
Assert.equal(retrieveInvestmentVault().investmentDuration, block.timestamp + 10 days, "Duration is not correct"),
Assert.equal(retrieveInvestmentVault().returnOnInvestment, 5, "Return on Investment is not correct"),
Assert.equal(retrieveInvestmentVault().initialized, true, "Initialization status is not correct")
);
}
/// #sender: account-1
function checkFailedSettingInitialInvestmentButWithUnautorizedAccount() public returns (bool) {
setInitialInvestmentVault(
10,
5,
acc1
);
return (Assert.ok(true, "True"));
}
function checkRetrieveCustomerInformation() public returns (bool) {
return Assert.equal(retrieveCustomerInformation(), acc1, "Customer information is wrong");
}
}
1.3. SimpleStorage_test.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
import "../SimpleStorage.sol";
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract testSuite is SimpleStorage {
SimpleStorage simpleStorage;
address acc0;
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
simpleStorage = new SimpleStorage();
acc0 = TestsAccounts.getAccount(0);
}
function checkMaintainerName() public returns (bool) {
return Assert.equal(simpleStorage.maintainerName(), "zxstim", "Maintainer name is not correct");
}
function checkVersion() public returns (bool) {
return Assert.equal(simpleStorage.version(), 1, "Version is not 1");
}
function checkDonationAddress() public returns (bool) {
return Assert.equal(simpleStorage.donationAddress(), 0xe3d25540BA6CED36a0ED5ce899b99B5963f43d3F, "Donation address is not correct");
}
/// #sender: account-0
function checkStoredPerson() public returns (bool, bool, bool, bool, bool, bool) {
Person memory person = storePerson("victor",30,true,10,2);
return (
Assert.equal(person.name, "victor", "Name is not correct"),
Assert.equal(person.age, 30, "Age is not correct"),
Assert.equal(person.overEighteen, true, "overEighteen status is not correct"),
Assert.equal(person.uuid, msg.sender, "Address is not correct"),
Assert.equal(person.assetValue, 10e18, "Asset value is not correct"),
Assert.equal(person.debtValue, 2e18, "Debt value is not correct")
);
}
/// #sender: account-0
function checkRetrivePersonWithAddress() public returns (bool, bool, bool, bool, bool, bool) {
Assert.ok(msg.sender == acc0, "caller should be default account i.e. acc0");
storePerson("victor",30,true,10,2);
return (
Assert.equal(retrievePerson(msg.sender).name, "victor", "Name is not correct"),
Assert.equal(retrievePerson(msg.sender).age, 30, "Age is not correct"),
Assert.equal(retrievePerson(msg.sender).overEighteen, true, "overEighteen status is not correct"),
Assert.equal(retrievePerson(msg.sender).uuid, msg.sender, "Address is not correct"),
Assert.equal(retrievePerson(msg.sender).assetValue, 10e18, "Asset value is not correct"),
Assert.equal(retrievePerson(msg.sender).debtValue, 2e18, "Debt value is not correct")
);
}
}
2. 测试 Solidity 文件
对Solidity文件进行全面测试,对于确保区块链项目的质量、可靠性和安全性至关重要。
以下是一些主要原因:
- 错误检测:测试有助于您识别并修复Solidity代码中的错误。代码错误可能导致异常行为,甚至造成资产损失。测试能帮助您及早发现这些错误,并在其引发严重问题前予以修复。
- 确保安全性:测试有助于您发现Solidity代码中的安全漏洞。安全漏洞可能使您的项目面临黑客攻击风险。通过测试,您可以识别这些漏洞并采取措施进行修复。
- 提高可靠性:测试有助于提升区块链项目的可靠性。当用户了解到项目经过全面测试后,他们将对项目产生更多信任,并更愿意使用它。
- 节省时间和金钱:测试有助于您从长远来看节省时间和金钱。及早修复错误将帮助您避免后期出现更严重且代价高昂的问题。
有多种不同的测试方法可用于测试Solidity文件。一些常用的方法包括:
- 单元测试:是一种测试代码中每个独立单元的方法。
- 集成测试:是一种测试不同代码单元如何协同工作的方法。
- 分叉测试:是一种在模拟真实环境的条件下测试代码的方法。
- 分阶段测试:指在真实环境(非生产环境)中测试代码的方法。选择合适的测试方法需根据项目具体需求而定。
以下是一些有效测试Solidity文件的技巧:
- 编写易于理解和维护的测试代码。
- 采用多种不同的测试方法。
- 自动化测试。
- 考虑使用专业测试工具。
如何测试?
Remix、Hardhat 或 Foundry 各自提供智能合约测试方案。您可参考以下详细文档:
3. 关于Solidity中合约的更多信息
3.1. 构造函数
构造函数是在智能合约初始化时立即执行的函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Base contract X
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
}3.2. 状态变量可见性
公共- 公共变量类似于内部变量(允许当前合约和继承合约访问),但会自动创建一个获取器函数以便外部合同也能访问它。内部- 该变量仅可被当前合约及其继承合约访问。这也是状态变量的默认可见性。私人- 该变量仅可由当前合约访问。
注:
该内部以及私人变量仅限制对其他变量的访问合同该变量的值对所有人可见。
3.3. 函数可见性
外部-函数只能从外部调用。公共-函数两者都可以被另一个调用函数在合同,也可以从外部调用。内部-函数只能由现有的合同或遗传的合同.私人-函数只能由当前合同.
3.4. 获取函数
函数 用于调用 公共 编译器自动创建的变量。也用于指代变量这一概念。 函数 用于查询要查看的变量。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}3.5. 常量与不可变状态变量
常数- 编译时(放入合约字节码)其值即被固定的变量。不可变的- 值可在执行期间被赋值的变量构造.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals = 18;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint decimals_, address ref) {
if (decimals_ != 0)
// Immutables are only immutable when deployed.
// At construction time they can be assigned to any number of times.
decimals = decimals_;
// Assignments to immutables can even access the environment.
maxBalance = ref.balance;
}
function isBalanceTooHigh(address other) public view returns (bool) {
return other.balance > maxBalance;
}
}3.6. 纯函数
函数 不读取或改变区块链的状态。或用于计算 函数.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}3.7. 可支付功能及地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Payable {
// Payable address can send Ether via transfer or send
address payable public owner;
// Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}
// Function to deposit Ether into this contract.
// Call this function along with some Ether.
// The balance of this contract will be automatically updated.
function deposit() public payable {}
// Call this function along with some Ether.
// The function will throw an error since this function is not payable.
function notPayable() public {}
// Function to withdraw all Ether from this contract.
function withdraw() public {
// get the amount of Ether stored in this contract
uint256 amount = address(this).balance;
// send all Ether to owner
(bool success,) = owner.call{value: amount}("");
require(success, "Failed to send Ether");
}
// Function to transfer Ether from this contract to address from input
function transfer(address payable _to, uint256 _amount) public {
// Note that "to" is declared as payable
(bool success,) = _to.call{value: _amount}("");
require(success, "Failed to send Ether");
}
}3.8. 以太接收与回退函数
A 合同 最多只能有一个 接收 函数,使用 接收() 外部可支付 { ... } (没有 函数 关键词). 该 函数 必须没有 论点,不能 返回 任何事物都必须拥有 外部 可见性以及 应付 状态可变性它可以是 虚拟它可以是 覆盖 并且它可以拥有 修饰词.
哪 函数 是 被, 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. 预言者
智能合约预言机是区块链与外部世界的桥梁。它为智能合约提供来自区块链外部的数据源,例如API接口、市场数据、气象数据等。
以下是一些使用Oracle实现智能合约的示例:
- 为去中心化市场(DeFi)提供价格数据:预言机可提供加密资产的价格数据,使交易者能够在去中心化交易所进行交易。
- 激活保险合同:甲骨文可提供保险事件(如事故或自然灾害)的相关数据,以触发保险赔付。
- 自动化流程:Oracle可用于自动化流程,例如账单支付或供应链管理。
Klaytn上的预言机列表:https://klaytn.foundation/ecosystem/?search=&cate=oracles-bridges&sort=abc
4. 铸造厂众筹
4.1. 框架铸造厂
事实上,Remix IDE 在功能方面存在诸多限制,因此我们将使用 Foundry——一个用于开发、测试和部署智能合约的框架。
4.2. 安装
访问网站GetFoundry.sh并按照说明操作。
4.3. 入门指南
访问并按照《Foundry Book》中的说明初始化项目。
4.4. Fund Me 项目
本练习基于 Patrick Collins 的Foundry FundMe代码库,但已更新以适应 Klaytn 的环境。
- 首次运行
forge init klaytn-fund-me - 然后我们将创建
FundMe.sol文件
// FundMe.sol
// SPDX-License-Identifier: MIT
// 1. Pragma
pragma solidity ^0.8.19;
// 2. Imports
// We import the orakl library so we can interact with oracle
import { IAggregator } from "@bisonai/orakl-contracts/src/v0.1/interfaces/IAggregator.sol";
// We import the PriceConverter library so we can calculate the KLAY value
import { PriceConverter } from "./PriceConverter.sol";
// 3. Interfaces, Libraries, Contracts
// Declaring error is not the Owner of the contract
error FundMe__NotOwner();
/**
* @title A sample Funding Contract
* @author Patrick Collins
* @notice This contract is for creating a sample funding contract
* @dev This implements price feeds as our library
*/
contract FundMe {
// Type Declarations
// The next line means
// use the PriceConverter library for variables with type uint256
using PriceConverter for uint256;
// State variables
// Declare a public constant MINIMUM_USD with value $5 but equal to peb so must multiply by 10^18
uint256 public constant MINIMUM_USD = 5 * 10 ** 18;
// Declare a private and immutable address with the name i_owner, i means immutable.
address private immutable i_owner;
// Declare a private array containing a list of people who fund ether with the name s_funders, s means storage.
address[] private s_funders;
// Declare a mapping between address and private uint256 linking the address with the fund amount.
mapping(address => uint256) private s_addressToAmountFunded;
// Declare contract AggregatorV3Interface private and assign it to the variable s_pricefeed, s means storage
IAggregator private s_priceFeed;
// Events (we have none!)
// Modifiers
// Declare an onlyOwner modifier to assign to a function that only the owner can call
modifier onlyOwner() {
// require(msg.sender == i_owner);
if (msg.sender != i_owner) revert FundMe__NotOwner();
_;
}
// Functions Order:
//// constructor
//// receive
//// fallback
//// external
//// public
//// internal
//// private
//// view / pure
// Declaring a constructor with an address for priceFeed implies that this is the address of the Oracle contract with IAggregator
constructor(address priceFeed) {
// Input the address into the interface and assign it to the variable s_priceFeed
s_priceFeed = IAggregator(priceFeed);
// Assign the variable i_owner to msg.sender (the person who deploys this contract)
i_owner = msg.sender;
}
/// @notice Funds our contract based on the KLAY/USDT price from Orakl
// Deposit to our contract based on ETH/USD price
function fund() public payable {
require(msg.value.getConversionRate(s_priceFeed) >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
// Then map the sender's address with msg.value in mapping s_addressToAmountFunded
s_addressToAmountFunded[msg.sender] += msg.value;
// Then add the sender address to the list of funders
s_funders.push(msg.sender);
}
function withdraw() public onlyOwner {
// Use for loop, starting from index 0 to index less than the length of the list, and index plus 1 for each loop
for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) {
// assign the address value at funderIndex in the s_funders list to the funder address
address funder = s_funders[funderIndex];
// Change the value of mapping s_addressToAmountFunded whose address is funder to 0, meaning this funder has withdrawn
s_addressToAmountFunded[funder] = 0;
}
// Create a new s_funders list with a new dynamic array (literally a list) of size 0
s_funders = new address[](0);
// Transfer vs call vs Send
// Transfer vs call vs Send
// - transfer (2300 gas, throws error if any)
// - send (2300 gas, returns bool for success or failure)
// - call (forward all gas or set gas, returns bool for success or failure)
// payable(msg.sender).transfer(address(this).balance);
// Send the entire balance of this contract to i_owner with no data in the transaction and return boolean success or not
(bool success,) = i_owner.call{value: address(this).balance}("");
// Require bool success true otherwise revert completely
require(success);
}
function cheaperWithdraw() public onlyOwner {
// Copy the list of s_funders from storage to memory, that is, load from global state to local state. Changing global state consumes more gas than local state
address[] memory funders = s_funders;
// mappings can't be in memory, sorry!
for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
address funder = funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
// payable(msg.sender).transfer(address(this).balance);
(bool success,) = i_owner.call{value: address(this).balance}("");
require(success);
}
/** Getter Functions */
// Functions are only used to GET information
/**
* @notice Gets the amount that an address has funded
* @param fundingAddress the address of the funder
* @return the amount funded
*/
function getAddressToAmountFunded(address fundingAddress) public view returns (uint256) {
return s_addressToAmountFunded[fundingAddress];
}
/**
* @notice Gets the funder at a specific index
* @param index the index of the funder
* @return the address of the funder
*/
function getFunder(uint256 index) public view returns (address) {
return s_funders[index];
}
/// @notice Gets the owner of the contract
function getOwner() public view returns (address) {
return i_owner;
}
/// @notice Gets the price feed
function getPriceFeed() public view returns (IAggregator) {
return s_priceFeed;
}
/// @notice Gets the decimals of the price feed
function getDecimals() public view returns (uint8) {
return s_priceFeed.decimals();
}
/// @notice Gets the description of the price feed
function getDescription() public view returns (string memory) {
return s_priceFeed.description();
}
}- 我们持续创造
价格转换器.sol文件
// PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// import IAggregator từ orakl repository
import { IAggregator } from "@bisonai/orakl-contracts/src/v0.1/interfaces/IAggregator.sol";
// Declare a library named PriceConverter
library PriceConverter {
// Declare function getPrice with input as contract interface and return uint256
function getPrice(IAggregator priceFeed) internal view returns (uint256) {
// gọi function latestRoundData() trong priceFeed
(, int256 answer,,,) = priceFeed.latestRoundData();
// Returns the ETH/USD rate with 18 digits (Oracle has 8 zeros so add 10 zeros)
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
}
// 1000000000
// call it get fiatConversionRate, since it assumes something about decimals
// It wouldn't work for every aggregator
// Convert KLAY amount to USD amount
// function getConversionRate takes input ethAmount with type uint256 and interface contract, returns uint256
function getConversionRate(uint256 ethAmount, IAggregator priceFeed) internal view returns (uint256) {
// First get the eth price using getPrice and assign it to the variable ethPrice
uint256 ethPrice = getPrice(priceFeed);
// Then multiply ethPrice by the amount of ether and divide by 18 zeros
// In solidity, we should multiply before dividing because there is no float
// This calculation is ethPrice (18 digits) * ethAmount (18 digits) / 18 digits to get back 18 digits.
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// Returns the USD value of the ether amount
// the actual ETH/USD conversation rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}- 处理 Foundry 的导入依赖项
forge install Bisonai/oraklforge install Cyfrin/foundry-devops添加 重新映射和 RPC 端点 至 铸造厂.toml 文件
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
"@bisonai/orakl-contracts/src/=lib/orakl/contracts/src/",
]
ffi = true
fs_permissions = [{ access = "read", path = "./broadcast" }]
[rpc_endpoints]
baobab = "${BAOBAB_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options- 添加
.env文件并添加猴面包树_远程过程调用URL猴面包树_远程过程调用URL可在Ankr、Allthatnodes等平台获取。
BAOBAB_RPC_URL=https://xxxxxx/xxxxx- 在
测试文件夹中创建3个子文件夹单位,整合,嘲弄和文件FundMeTest.t.sol,交互测试.t.sol,模拟数据源聚合器.sol
└── 测试
├── 集成测试
│ └── 交互测试.t.sol
├── 模拟测试
│ └── 模拟数据聚合器.sol
└── 单元测试
└── 众筹测试.t.sol复制3个文件的内容
FundMeTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { DeployFundMe } from "../../script/DeployFundMe.s.sol";
import { FundMe } from "../../src/FundMe.sol";
import { HelperConfig } from "../../script/HelperConfig.s.sol";
import { Test, console } from "forge-std/Test.sol";
import { StdCheats } from "forge-std/StdCheats.sol";
contract FundMeTest is StdCheats, Test {
FundMe public fundMe;
HelperConfig public helperConfig;
uint256 public constant SEND_VALUE = 0.1 ether; // just a value to make sure we are sending enough!
uint256 public constant STARTING_USER_BALANCE = 10 ether;
uint256 public constant GAS_PRICE = 1;
address public constant USER = address(1);
// uint256 public constant SEND_VALUE = 1e18;
// uint256 public constant SEND_VALUE = 1_000_000_000_000_000_000;
// uint256 public constant SEND_VALUE = 1000000000000000000;
function setUp() external {
DeployFundMe deployer = new DeployFundMe();
(fundMe, helperConfig) = deployer.run();
vm.deal(USER, STARTING_USER_BALANCE);
}
function testPriceFeedSetCorrectly() public view {
address retreivedPriceFeed = address(fundMe.getPriceFeed());
(address expectedPriceFeed) = helperConfig.activeNetworkConfig();
assertEq(retreivedPriceFeed, expectedPriceFeed);
}
function testFundFailsWithoutEnoughETH() public {
vm.expectRevert();
fundMe.fund();
}
function testFundUpdatesFundedDataStructure() public {
vm.startPrank(USER);
fundMe.fund{value: SEND_VALUE}();
vm.stopPrank();
uint256 amountFunded = fundMe.getAddressToAmountFunded(USER);
assertEq(amountFunded, SEND_VALUE);
}
function testAddsFunderToArrayOfFunders() public {
vm.startPrank(USER);
fundMe.fund{value: SEND_VALUE}();
vm.stopPrank();
address funder = fundMe.getFunder(0);
assertEq(funder, USER);
}
// https://twitter.com/PaulRBerg/status/1624763320539525121
modifier funded() {
vm.prank(USER);
fundMe.fund{value: SEND_VALUE}();
assert(address(fundMe).balance > 0);
_;
}
function testOnlyOwnerCanWithdraw() public funded {
vm.expectRevert();
fundMe.withdraw();
}
function testWithdrawFromASingleFunder() public funded {
// Arrange
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
// vm.txGasPrice(GAS_PRICE);
// uint256 gasStart = gasleft();
// // Act
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
// uint256 gasEnd = gasleft();
// uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
// Assert
uint256 endingFundMeBalance = address(fundMe).balance;
uint256 endingOwnerBalance = fundMe.getOwner().balance;
assertEq(endingFundMeBalance, 0);
assertEq(
startingFundMeBalance + startingOwnerBalance,
endingOwnerBalance // + gasUsed
);
}
// Can we do our withdraw function a cheaper way?
function testWithdrawFromMultipleFunders() public funded {
uint160 numberOfFunders = 10;
uint160 startingFunderIndex = 2;
for (uint160 i = startingFunderIndex; i < numberOfFunders + startingFunderIndex; i++) {
// we get hoax from stdcheats
// prank + deal
hoax(address(i), STARTING_USER_BALANCE);
fundMe.fund{value: SEND_VALUE}();
}
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
assert(address(fundMe).balance == 0);
assert(startingFundMeBalance + startingOwnerBalance == fundMe.getOwner().balance);
assert((numberOfFunders + 1) * SEND_VALUE == fundMe.getOwner().balance - startingOwnerBalance);
}
}模拟数据源聚合器.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title MockV3Aggregator
* @notice Based on the FluxAggregator contract
* @notice Use this contract when you need to test
* other contract's ability to read data from an
* aggregator contract, but how the aggregator got
* its answer is unimportant
*/
contract MockDataFeedAggregator {
uint256 public constant version = 4;
uint8 public decimals;
int256 public latestAnswer;
uint256 public latestTimestamp;
uint256 public latestRound;
mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
mapping(uint256 => uint256) private getStartedAt;
constructor(uint8 _decimals, int256 _initialAnswer) {
decimals = _decimals;
updateAnswer(_initialAnswer);
}
function updateAnswer(int256 _answer) public {
latestAnswer = _answer;
latestTimestamp = block.timestamp;
latestRound++;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = block.timestamp;
getStartedAt[latestRound] = block.timestamp;
}
function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
latestRound = _roundId;
latestAnswer = _answer;
latestTimestamp = _timestamp;
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = _timestamp;
getStartedAt[latestRound] = _startedAt;
}
function getRoundData(uint80 _roundId)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
}
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (
uint80(latestRound),
getAnswer[latestRound],
getStartedAt[latestRound],
getTimestamp[latestRound],
uint80(latestRound)
);
}
function description() external pure returns (string memory) {
return "v0.6/test/mock/MockV3Aggregator.sol";
}
}交互测试.t.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import { DeployFundMe } from "../../script/DeployFundMe.s.sol";
import { FundFundMe, WithdrawFundMe } from "../../script/Interactions.s.sol";
import { FundMe } from "../../src/FundMe.sol";
import { HelperConfig } from "../../script/HelperConfig.s.sol";
import { Test, console } from "forge-std/Test.sol";
import { StdCheats } from "forge-std/StdCheats.sol";
contract InteractionsTest is StdCheats, Test {
FundMe public fundMe;
HelperConfig public helperConfig;
uint256 public constant SEND_VALUE = 0.1 ether; // just a value to make sure we are sending enough!
uint256 public constant STARTING_USER_BALANCE = 10 ether;
uint256 public constant GAS_PRICE = 1;
address public constant USER = address(1);
// uint256 public constant SEND_VALUE = 1e18;
// uint256 public constant SEND_VALUE = 1_000_000_000_000_000_000;
// uint256 public constant SEND_VALUE = 1000000000000000000;
function setUp() external {
DeployFundMe deployer = new DeployFundMe();
(fundMe, helperConfig) = deployer.run();
vm.deal(USER, STARTING_USER_BALANCE);
}
function testUserCanFundAndOwnerWithdraw() public {
FundFundMe fundFundMe = new FundFundMe();
fundFundMe.fundFundMe(address(fundMe));
WithdrawFundMe withdrawFundMe = new WithdrawFundMe();
withdrawFundMe.withdrawFundMe(address(fundMe));
assert(address(fundMe).balance == 0);
}
}- 然后我们去
脚本文件夹并创建文件部署FundMe.s.sol,HelperConfig.s.sol以及交互作用.s.sol
└──脚本
├── DeployFundMe.s.sol
├── HelperConfig.s.sol
└── Interactions.s.sol部署FundMe.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)});
}
}交互作用.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);
}
}- 部署 使用此命令部署至猴面包树测试网
forge script script/DeployFundMe.s.sol --rpc-url $BAOBAB_RPC_URL --account $WALLET_NAME --sender $SENDER_ADDRESS --broadcast --gas-estimate-multiplier 200--燃气估算系数 200- 是将燃气预估值乘以2,因为燃气不足可能导致交易失败--发件人 $SENDER_ADDRESS- 替换$发件人地址附上您的地址--账户 $WALLET_NAME- 你可以使用命令进行设置铸造钱包新以及铸造钱包导入. 替换$WALLET_NAME使用您保存的密钥库名称
5. 安全帽众筹
安全帽框架
5.1. 搭建开发环境
mkdir安全帽众筹项目
cd 安全帽众筹项目要使用 hardhat,我们需要搭建开发环境并安装 hardhat。请按以下步骤操作:
步骤 1:创建项目目录
步骤 2:初始化一个 npm 项目
在终端中粘贴此命令以创建 package.json 文件
npm init -y步骤3:安装hardhat及其他依赖项
- 在终端中粘贴以下代码以安装 hardhat
npm install --save-dev hardhat- 将以下代码粘贴以安装其他依赖项
npm install dotenv @bisonai/orakl-contracts步骤4:初始化硬帽项目
npx hardhat init请务必按照终端中高亮显示的提示操作。在本项目中,我们选择了hardhat javascript 项目并安装了hardhat-toolbox。
初始化硬帽项目后,您的当前目录应包含:
1. 合同/ – 此 文件夹包含智能合约代码。
2. ignition/ – 此 文件夹包含将合约部署到区块链网络的代码。
3. test/ – 此 文件夹包含用于测试智能合约的所有单元测试。
4. hardhat.config.js – 此 文件包含 工作 Hardhat Hardhat、部署和验证 的 FundMe合约。步骤5:创建.env文件
现在在项目文件夹中创建您的.env文件。该文件有助于我们从环境中加载环境变量。 .env 排成队列 process.env.
- 在终端中粘贴此命令以创建 .env 文件
触摸 .env- 创建文件后,请将我们的.env文件配置如下所示。Kairos RPC网址可在此处找到:
KAIA_KAIROS_URL= "您的Kairos RPC链接"
PRIVATE_KEY= "从MetaMask钱包复制的私钥"步骤6:设置安全帽配置
修改您的 安全帽配置文件 具有以下配置:
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config()
module.exports = {
solidity: "0.8.24",
networks: {
kairos: {
url: process.env.KAIA_KAIROS_URL || "",
gasPrice: 250000000000,
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
}
},
etherscan: {
apiKey: {
kairos: "unnecessary",
},
customChains: [
{
network: "kairos",
chainId: 1001,
urls: {
apiURL: "https://api-baobab.klaytnscope.com/api",
browserURL: "https://baobab.klaytnscope.com",
},
},
]
}
};
既然开发环境已全部就绪,现在就让我们着手编写Fundme智能合约吧。
5.2. 创建FundMe智能合约
在合同文件夹中,您将创建 FundMe.sol 以及 价格转换器.sol 文件分别。
FundMe.sol
// FundMe.sol
// SPDX-License-Identifier: MIT
// 1. Pragma
pragma solidity ^0.8.19;
// 2. Imports
// We import the orakl library so we can interact with oracle
import { IFeedProxy } from "@bisonai/orakl-contracts/v0.2/src/interfaces/IFeedProxy.sol";
// We import the PriceConverter library so we can calculate the KLAY value
import { PriceConverter } from "./PriceConverter.sol";
// 3. Interfaces, Libraries, Contracts
// Declaring error is not the Owner of the contract
error FundMe__NotOwner();
/**
* @title A sample Funding Contract
* @author Patrick Collins
* @notice This contract is for creating a sample funding contract
* @dev This implements price feeds as our library
*/
contract FundMe {
// Type Declarations
// The next line means
// use the PriceConverter library for variables with type uint256
using PriceConverter for uint256;
// State variables
// Declare a public constant MINIMUM_USD with value $5 but equal to peb so must multiply by 10^18
uint256 public constant MINIMUM_USD = 5 * 10 ** 18;
// Declare a private and immutable address with the name i_owner, i means immutable.
address private immutable i_owner;
// Declare a private array containing a list of people who fund ether with the name s_funders, s means storage.
address[] private s_funders;
// Declare a mapping between address and private uint256 linking the address with the fund amount.
mapping(address => uint256) private s_addressToAmountFunded;
// Declare contract AggregatorV2Interface internal and assign it to the variable s_dataFeed, s means storage
IFeedProxy internal s_dataFeed;
// Events (we have none!)
// Modifiers
// Declare an onlyOwner modifier to assign to a function that only the owner can call
modifier onlyOwner() {
// require(msg.sender == i_owner);
if (msg.sender != i_owner) revert FundMe__NotOwner();
_;
}
// Functions Order:
//// constructor
//// receive
//// fallback
//// external
//// public
//// internal
//// private
//// view / pure
// Declaring a constructor with an address for priceFeed implies that this is the address of the Oracle contract with IAggregator
constructor(address feedProxy) {
// Input the address into the interface and assign it to the variable s_priceFeed
// s_priceFeed = IAggregator(priceFeed);
s_dataFeed = IFeedProxy(feedProxy);
// Assign the variable i_owner to msg.sender (the person who deploys this contract)
i_owner = msg.sender;
}
/// @notice Funds our contract based on the KLAY/USDT price from Orakl
// Deposit to our contract based on ETH/USD price
function fund() public payable {
require(msg.value.getConversionRate(s_dataFeed) >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
// Then map the sender's address with msg.value in mapping s_addressToAmountFunded
s_addressToAmountFunded[msg.sender] += msg.value;
// Then add the sender address to the list of funders
s_funders.push(msg.sender);
}
function withdraw() public onlyOwner {
// Use for loop, starting from index 0 to index less than the length of the list, and index plus 1 for each loop
for (uint256 funderIndex = 0; funderIndex < s_funders.length; funderIndex++) {
// assign the address value at funderIndex in the s_funders list to the funder address
address funder = s_funders[funderIndex];
// Change the value of mapping s_addressToAmountFunded whose address is funder to 0, meaning this funder has withdrawn
s_addressToAmountFunded[funder] = 0;
}
// Create a new s_funders list with a new dynamic array (literally a list) of size 0
s_funders = new address[](0);
// Transfer vs call vs Send
// Transfer vs call vs Send
// - transfer (2300 gas, throws error if any)
// - send (2300 gas, returns bool for success or failure)
// - call (forward all gas or set gas, returns bool for success or failure)
// payable(msg.sender).transfer(address(this).balance);
// Send the entire balance of this contract to i_owner with no data in the transaction and return boolean success or not
(bool success,) = i_owner.call{value: address(this).balance}("");
// Require bool success true otherwise revert completely
require(success);
}
function cheaperWithdraw() public onlyOwner {
// Copy the list of s_funders from storage to memory, that is, load from global state to local state. Changing global state consumes more gas than local state
address[] memory funders = s_funders;
// mappings can't be in memory, sorry!
for (uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
address funder = funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
// payable(msg.sender).transfer(address(this).balance);
(bool success,) = i_owner.call{value: address(this).balance}("");
require(success);
}
/** Getter Functions */
// Functions are only used to GET information
/**
* @notice Gets the amount that an address has funded
* @param fundingAddress the address of the funder
* @return the amount funded
*/
function getAddressToAmountFunded(address fundingAddress) public view returns (uint256) {
return s_addressToAmountFunded[fundingAddress];
}
/**
* @notice Gets the funder at a specific index
* @param index the index of the funder
* @return the address of the funder
*/
function getFunder(uint256 index) public view returns (address) {
return s_funders[index];
}
/// @notice Gets the owner of the contract
function getOwner() public view returns (address) {
return i_owner;
}
/// @notice Gets the price feed
function getPriceFeed() public view returns (address) {
return s_dataFeed.getFeed();
}
/// @notice Gets the decimals of the price feed
function getDecimals() public view returns (uint8) {
return s_dataFeed.decimals();
}
}价格转换器.sol
// PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// import IAggregator từ orakl repository
import { IFeedProxy } from "@bisonai/orakl-contracts/v0.2/src/interfaces/IFeedProxy.sol";
// Declare a library named PriceConverter
library PriceConverter {
// Declare function getPrice with input as contract interface and return uint256
function getPrice(IFeedProxy dataFeed) internal view returns (uint256) {
// gọi function latestRoundData() trong priceFeed
(, int256 answer,) = dataFeed.latestRoundData();
// Returns the ETH/USD rate with 18 digits (Oracle has 8 zeros so add 10 zeros)
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
}
// 1000000000
// call it get fiatConversionRate, since it assumes something about decimals
// It wouldn't work for every aggregator
// Convert KLAY amount to USD amount
// function getConversionRate takes input ethAmount with type uint256 and interface contract, returns uint256
function getConversionRate(uint256 ethAmount, IFeedProxy dataFeed) internal view returns (uint256) {
// First get the eth price using getPrice and assign it to the variable ethPrice
uint256 ethPrice = getPrice(dataFeed);
// Then multiply ethPrice by the amount of ether and divide by 18 zeros
// In solidity, we should multiply before dividing because there is no float
// This calculation is ethPrice (18 digits) * ethAmount (18 digits) / 18 digits to get back 18 digits.
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// Returns the USD value of the ether amount
// the actual ETH/USD conversation rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}5.3. 测试FundMe智能合约
第一步: 在资源管理器窗格中,选择测试文件夹,然后单击新建文件按钮创建一个名为 Fundme.js
第二步: 创建一个 模拟数据源聚合器.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";
}
}第三步: 将以下代码复制并粘贴到 Fundme.js 文件:
// Fundme.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
describe("FundMe", function () {
async function deployContractsFixture() {
const [deployer, addr1, addr2] = await ethers.getSigners();
const MockDataFeedAggregator = await ethers.getContractFactory("MockDataFeedAggregator");
const mockPriceFeed = await MockDataFeedAggregator.connect(deployer).deploy(8, 2000 * 10 ** 8); // Example price of 2000 USD with 8 decimals
await mockPriceFeed.waitForDeployment(); // Ensure the contract is deployed
// Use fully qualified name for FundMe contract
const FundMe = await ethers.getContractFactory("contracts/FundMe.sol:FundMe");
const fundMe = await FundMe.connect(deployer).deploy(mockPriceFeed.target);
await fundMe.waitForDeployment(); // Ensure the contract is deployed
return { fundMe, mockPriceFeed, deployer, addr1, addr2 };
}
describe("Deployment", function () {
it("Should set the right owner", async function () {
const { fundMe, deployer } = await loadFixture(deployContractsFixture);
expect(await fundMe.getOwner()).to.equal(deployer.address);
});
});
describe("Fund", function () {
it("Should accept funds", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("1"); // 1 ETH
await fundMe.connect(addr1).fund({ value: sendValue });
expect(await fundMe.getAddressToAmountFunded(addr1.address)).to.equal(sendValue);
});
it("Should require a minimum amount in USD", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("0.001"); // 0.001 ETH, less than minimum
await expect(fundMe.connect(addr1).fund({ value: sendValue })).to.be.revertedWith(
"You need to spend more ETH!"
);
});
});
describe("Withdraw", function () {
it("Should withdraw ETH correctly", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("1"); // 1 ETH
await fundMe.connect(addr1).fund({ value: sendValue });
await fundMe.withdraw();
expect(await ethers.provider.getBalance(fundMe.target)).to.equal(0);
});
it("Should only allow the owner to withdraw", async function () {
const { fundMe, addr1 } = await loadFixture(deployContractsFixture);
const sendValue = ethers.parseEther("1"); // 1 ETH
await fundMe.connect(addr1).fund({ value: sendValue });
await expect(fundMe.connect(addr1).withdraw()).to.be.revertedWithCustomError(
fundMe,
"FundMe__NotOwner"
);
});
});
});
步骤4:要运行测试,请打开终端并执行以下命令:
5.4. 部署智能合约
第一步在资源管理器窗格中,选择 点火/模块 文件夹,然后点击新建文件按钮创建一个名为 Fundme.js
步骤 2:将以下代码复制并粘贴到文件中。
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const PRICE_FEED_ADDRESS = "0x1408cb13d84ba8cb533fdf332db5d78290b071c9";
module.exports = buildModule("FundMeModule", (m) => {
const priceFeedAddr = m.getParameter("_priceFeed", PRICE_FEED_ADDRESS);
const fundMe = m.contract("FundMe", [priceFeedAddr], {});
return { fundMe };
});步骤3:在终端中运行以下命令,指示Hardhat将您的Fundme合约部署到Kaia测试网(Kairos)上。
npx hardhat ignition deploy ignition/modules/Fundme.js --network kairos5.5. 智能合约验证
在终端中复制并粘贴以下代码:
// example
// npx hardhat verify –network <network> <deployed_address> <parameters>
npx hardhat verify --network kairos 0xa9a6f38b67d7ba2b716d0d2bd21974e2149df7ef 0xf0d6Ccdd18B8A7108b901af872021109C27095bA