在之前的文章中,我们已经简单介绍了Foundry的基本概念和安装方法。本文将以一个简单的 ERC20 合约为例,介绍如何使用Foundry进行合约的编写。
创建项目
首先,我们需要创建一个新的项目,命令如下:
$ forge init MyToken --vscode
$ cd MyToken
为方便vscode使用,使用 –vscode 参数,此举将创建一个包含 Solidity 设置的
.vscode/settings.json
文件,并生成一个remappings.txt
文件。
安装依赖
编写 ERC20 合约,我们需要借助 OpenZeppelin,使用下面的命令安装依赖:
$ forge install OpenZeppelin/openzeppelin-contracts
$ forge remappings > remappings.txt
forge install
命令会自动安装最新版的OpenZeppelin/openzeppelin-contracts
库到lib
目录下forge remappings
更新remappings.txt
编写合约
使用 forge init
创建的示例项目中包含一个简单的 Counter.sol
合约,现在删除它,编写我们自己的合约,在 src
目录下创建我们的合约文件 MyToken.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MyToken
* @dev 一个基于OpenZeppelin的ERC20代币合约示例,继承了ERC20标准和Ownable权限管理
*/
contract MyToken is ERC20, Ownable {
/**
* @dev constructor, initializes the token name and symbol, and pre-mints a certain amount of tokens for the deployer
* @param name token
* @param symbol of the token
*/
constructor(string memory name, string memory symbol) ERC20(name, symbol) Ownable(msg.sender) {
// 在合约部署时铸造100万个代币,假设每个代币有18个小数位
_mint(msg.sender, 1000000 * 10 ** decimals());
}
/**
* @dev 合约拥有者调用的函数,可以冻结账户
* @param account 需要冻结的账户地址
* @notice 仅合约拥有者(部署者)可以调用此函数
*/
function freezeAccount(address account) external onlyOwner {
// 这里可以实现冻结账户的逻辑
// 比如将账户标记为冻结状态,以禁止转账或其他操作
}
// 可根据需要添加更多的功能
}
编写测试合约
在合约部署之前,我们还需要对合约进行测试,以确保其正常运行。在 test
目录下创建测试文件 MyToken.t.sol
,编写测试用例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken public myToken;
address public owner;
address public addr1;
uint256 public initialBalance = 1000000 * 10 ** 18;
// 在每个测试之前执行
function setUp() public {
// 部署合约,并为部署者铸造初始代币
owner = address(this);
addr1 = address(0x123);
myToken = new MyToken("MyToken", "MTK");
// 验证初始余额
assertEq(myToken.balanceOf(owner), initialBalance);
}
// 测试代币的名称和符号
function testTokenNameAndSymbol() public view {
assertEq(myToken.name(), "MyToken");
assertEq(myToken.symbol(), "MTK");
}
// 测试代币的总供应量
function testTotalSupply() public view {
assertEq(myToken.totalSupply(), initialBalance);
}
// 测试转账功能
function testTransfer() public {
uint256 transferAmount = 100 * 10 ** 18; // 转账100个代币
myToken.transfer(addr1, transferAmount);
// 验证余额变化
assertEq(myToken.balanceOf(owner), initialBalance - transferAmount);
assertEq(myToken.balanceOf(addr1), transferAmount);
}
// 测试代币铸造功能,确认部署时代币已铸造
function testInitialMint() public view {
assertEq(myToken.balanceOf(owner), initialBalance);
}
// call `freezeAccount`
function testOnlyOwnerCanFreeze() public {
vm.startPrank(addr1);
// Only owner can freeze account
vm.expectRevert(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector,
addr1
)
);
myToken.freezeAccount(addr1);
vm.stopPrank();
}
// 测试其他非拥有者无法调用 `freezeAccount`
function testNonOwnerCannotFreeze() public {
address nonOwner = address(0x456);
vm.startPrank(nonOwner);
vm.expectRevert(
abi.encodeWithSelector(
Ownable.OwnableUnauthorizedAccount.selector,
nonOwner
)
);
myToken.freezeAccount(addr1); // 只有拥有者可以冻结账户
vm.stopPrank();
}
}
执行测试
完成测试合约编写后,执行下面的命令进行测试:
$ forge test
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.28
[⠑] Solc 0.8.28 finished in 517.63ms
Compiler run successful!
Ran 6 tests for test/MyToken.t.sol:MyTokenTest
[PASS] testInitialMint() (gas: 15006)
[PASS] testNonOwnerCannotFreeze() (gas: 13763)
[PASS] testOnlyOwnerCanFreeze() (gas: 14025)
[PASS] testTokenNameAndSymbol() (gas: 16822)
[PASS] testTotalSupply() (gas: 12603)
[PASS] testTransfer() (gas: 47312)
Suite result: ok. 6 passed; 0 failed; 0 skipped; finished in 4.02ms (243.60µs CPU time)
Ran 1 test suite in 5.13ms (4.02ms CPU time): 6 tests passed, 0 failed, 0 skipped (6 total tests)
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意
腾讯云开发者社区:孟斯特
—