在之前的文章中,我们已经简单介绍了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: 恋水无意
腾讯云开发者社区:孟斯特