使用 Foundry 部署 ERC20 合约

请在GitHub参阅完整的foundry-erc20代码库。

本节将展示如何使用Foundry创建新的Solidity合约,配置Berachain网络详细信息,将合约部署到Berachain,以及验证合约。

先决条件

开始之前,请确保你的本地设备上满足以下条件:

创建ERC20合约代码设置

首先,为ERC20合约创建一个新的文件夹:

mkdir create-erc20-contract-using-foundry;
cd create-erc20-contract-using-foundry;

然后,运行以下代码,创建由Foundry定义的初始ERC20合约模板:

# FROM: ./create-erc20-contract-using-foundry

forge init; # forge init --force; # if there is already an existing .git repository associated

# [Expected Output]:
# ...
# Resolving deltas: 100% (129/129), done.
#     Installed forge-std v1.7.1
#     Initialized forge project

如果模板创建成功,会显示以下代码结构:

# FROM: ./create-erc20-contract-using-foundry
.
├── README.md
├── foundry.toml
├── lib
   └── forge-std
├── script
   └── Counter.s.sol
├── src
   └── Counter.sol
└── test
    └── Counter.t.sol

现在,所有代码已设置完成,运行以下代码,安装来自OpenZeppelin的ERC20合约所需的依赖项:

# FROM: ./create-erc20-contract-using-foundry

forge install OpenZeppelin/openzeppelin-contracts;
# If existing git setup run:
# forge install OpenZeppelin/openzeppelin-contracts --no-commit;

# [Expected Output]:
# ...
# Resolving deltas: 100% (129/129), done.
#     Installed openzeppelin-contracts v5.0.0

创建ERC20合约

开始之前,请将现有的src/Counter.sol转换为新的BingBongToken.sol,并将代码替换为以下 Solidity代码:

# FROM: ./create-erc20-contract-using-foundry

mv src/Counter.sol src/BingBongToken.sol;

文件位置./src/BingBongToken.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract BingBongToken is ERC20 {
    /**
     * @dev Init constructor for setting token name and symbol
     */
    constructor(string memory name_, string memory symbol_, uint256 mintedTokens_) ERC20(name_, symbol_) {
        _mint(msg.sender, mintedTokens_);
    }
}

运行以下代码,测试编译是否正确:

# FROM: ./create-erc20-contract-using-foundry

forge compile;

# [Expected Error Output]:
# [⠊] Compiling...
# [⠒] Unable to resolve imports:
#       "../src/Counter.sol" in "/path/to/create-erc20-contract-using-foundry/test/Counter.t.sol"
#  ...

如果出现上方显示的Expected Error Output,原因是引用了一个不存在的文件。为了解决这个问题,需要将其重命名为BingBongToken.t.sol,并替换一些占位符代码:

# FROM: ./create-erc20-contract-using-foundry

mv test/Counter.t.sol test/BingBongToken.t.sol;

文件位置./test/BingBongToken.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import {BingBongToken} from "../src/BingBongToken.sol";

contract BingBongTokenTest is Test {

}

现在,运行forge compile时,应该显示以下结果:

# FROM: ./create-erc20-contract-using-foundry

forge compile;

# [Expected Output]:
# [⠢] Compiling...
# [⠰] Compiling 27 files with 0.8.21
# [⠃] Solc 0.8.21 finished in 6.25s
# Compiler run successful!

测试ERC20合约

使用重命名的BingBongToken.t.sol文件,添加以下测试代码,该代码覆盖广泛的ERC20合约测试。

请检查每项测试,以便更加了解如何应对并成功处理各种情况。

文件位置./test/BingBongToken.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2, stdError} from "forge-std/Test.sol";
import {BingBongToken} from "../src/BingBongToken.sol";

contract BingBongTokenTest is Test {
    // Variables
    BingBongToken public token;
    address supplyOwnerAddress = makeAddr("BerachainWalletUser"); // 0xE3284cB941608AA9E65F7EDdbb50c461D936622f
    address randomWalletAddress = makeAddr("GiveMeTokens"); // 0x187A660c372Fa04D09C1A71f2927911e62e98a89
    address anotherWalletAddress = makeAddr("AnotherAddress"); // 0x0F3B9cC98eef350B12D5b7a338D8B76c2F9a92CC
    error ERC20InvalidReceiver(address receiver);

    // Initial Read Tests
    // ========================================================
    /**
     * @dev Initial contract setup
     */
    function setUp() public {
        vm.prank(supplyOwnerAddress);
        token = new BingBongToken("BingBong Token", "BBT", 10000);
    }

    /**
     * @dev Test initiatted token name
     */
    function test_name() public {
        assertEq(token.name(), "BingBong Token");
    }

    /**
     * @dev Test initiatted token symbol
     */
    function test_symbol() public {
        assertEq(token.symbol(), "BBT");
    }

    /**
     * @dev Test default decimals
     */
    function test_decimals() public {
        assertEq(token.decimals(), 18);
    }

    /**
     * @dev Test initial total token supply
     */
    function test_totalSupply() public {
        assertEq(token.totalSupply(), 10000);
    }

    /**
     * @dev Test initial random account balance
     */
    function test_balanceOfAddress0() public {
        assertEq(token.balanceOf(address(0)), 0);
    }

    /**
     * @dev Test account balance of original deployer
     */
    function test_balanceOfAddressSupplyOwner() public {
        assertEq(token.balanceOf(supplyOwnerAddress), 10000);
    }

    /**
     * @dev Test Revert transfer to sender as 0x0
     */
    function test_transferRevertInvalidSender() public {
        vm.prank(address(0));
        vm.expectRevert(abi.encodeWithSignature("ERC20InvalidSender(address)", address(0)));
        token.transfer(randomWalletAddress, 100);
    }

    /**
     * @dev Test Revert transfer to receiver as 0x0
     */
    function test_transferRevertInvalidReceiver() public {
        vm.prank(supplyOwnerAddress);
        vm.expectRevert(abi.encodeWithSignature("ERC20InvalidReceiver(address)", address(0)));
        token.transfer(address(0), 100);
    }

    /**
     * @dev Test Revert transfer to sender with insufficient balance
     */
    function test_transferRevertInsufficientBalance() public {
        vm.prank(randomWalletAddress);
        // NOTE: Make sure to keep this string for `encodeWithSignature` free of spaces for the string (" ")
        vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientBalance(address,uint256,uint256)", randomWalletAddress, 0, 100));
        token.transfer(supplyOwnerAddress, 100);
    }

    /**
     * @dev Test transfer to receiver from sender with sufficient balance
     */
    function test_transfer() public {
        vm.prank(supplyOwnerAddress);
        assertEq(token.transfer(randomWalletAddress, 100), true);
        assertEq(token.balanceOf(randomWalletAddress), 100);
        assertEq(token.balanceOf(supplyOwnerAddress), 10000 - 100);
    }

    /**
     * @dev Test allowance of random address for supplyOwner
     */
    function test_allowance() public {
        assertEq(token.allowance(supplyOwnerAddress, randomWalletAddress), 0);
    }

    /**
     * @dev Test Revert approve of owner as 0x0
     */
    function test_approveRevertInvalidApprover() public {
        vm.prank(address(0));
        vm.expectRevert(abi.encodeWithSignature("ERC20InvalidApprover(address)", address(0)));
        token.approve(randomWalletAddress, 100);
    }

    /**
     * @dev Test Revert approve of spender as 0x0
     */
    function test_approveRevertInvalidSpender() public {
        vm.prank(supplyOwnerAddress);
        vm.expectRevert(abi.encodeWithSignature("ERC20InvalidSpender(address)", address(0)));
        token.approve(address(0), 100);
    }

    /**
     * @dev Test approve of spender for 0 and 50
     */
    function test_approve() public {
        vm.prank(supplyOwnerAddress);
        assertEq(token.approve(randomWalletAddress, 0), true);
        assertEq(token.approve(randomWalletAddress, 50), true);
    }

    /**
     * @dev Test Revert transferFrom of spender with 0 approveed
     */
    function test_transferFromRevertInsufficientAllowanceFor0x0() public {
        vm.prank(supplyOwnerAddress);
        vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientAllowance(address,uint256,uint256)", supplyOwnerAddress, 0, 100));
        token.transferFrom(randomWalletAddress, address(0), 100);
    }

    /**
     * @dev Test Revert transferFrom of spender transferring to 0x0
     */
    function test_transferFromRevertInvalidReceiver() public {
        // Setup
        vm.prank(supplyOwnerAddress);
        token.approve(randomWalletAddress, 30);

        // Test
        vm.prank(randomWalletAddress);
        vm.expectRevert(abi.encodeWithSignature("ERC20InvalidReceiver(address)", address(0)));
        token.transferFrom(supplyOwnerAddress, address(0), 30);
    }

    /**
     * @dev Test Revert transferFrom of spender transferring 50/30 approved
     */
    function test_transferFromRevertInsufficientAllowance() public {
        // Setup
        vm.prank(supplyOwnerAddress);
        token.approve(randomWalletAddress, 30);

        // Test
        vm.prank(randomWalletAddress);
        vm.expectRevert(abi.encodeWithSignature("ERC20InsufficientAllowance(address,uint256,uint256)", randomWalletAddress, 30, 50));
        token.transferFrom(supplyOwnerAddress, anotherWalletAddress, 50);
    }

    /**
     * @dev Test transferFrom of spender 10/30 approved
     */
    function test_transferFrom() public {
        // Setup
        vm.prank(supplyOwnerAddress);
        token.approve(randomWalletAddress, 30);

        // Test
        vm.prank(randomWalletAddress);
        assertEq(token.transferFrom(supplyOwnerAddress, anotherWalletAddress, 10), true);
        assertEq(token.balanceOf(anotherWalletAddress), 10);
        assertEq(token.balanceOf(supplyOwnerAddress), 10000 - 10);
        assertEq(token.allowance(supplyOwnerAddress, randomWalletAddress), 30 - 10);
    }
}

接下来,编译代码,运行测试,检查各项测试是否通过。

# FROM: ./create-erc20-contract-using-foundry

forge test -vvv; # v stands for verbose and multiple vvv allow for more details for tests

# [Expected Output]:
# [⠰] Compiling...
# No files changed, compilation skipped
#
# Running 18 tests for test/BingBongToken.t.sol:BingBongTokenTest
# [PASS] test_allowance() (gas: 12341)
# [PASS] test_approve() (gas: 42814)
# [PASS] test_approveRevertInvalidApprover() (gas: 11685)
# [PASS] test_approveRevertInvalidSpender() (gas: 11737)
# [PASS] test_balanceOfAddress0() (gas: 7810)
# [PASS] test_balanceOfAddressSupplyOwner() (gas: 9893)
# [PASS] test_decimals() (gas: 5481)
# [PASS] test_name() (gas: 9541)
# [PASS] test_symbol() (gas: 9650)
# [PASS] test_totalSupply() (gas: 7546)
# [PASS] test_transfer() (gas: 44880)
# [PASS] test_transferFrom() (gas: 75384)
# [PASS] test_transferFromRevertInsufficientAllowance() (gas: 42626)
# [PASS] test_transferFromRevertInsufficientAllowanceFor0x0() (gas: 16597)
# [PASS] test_transferFromRevertInvalidReceiver() (gas: 28334)
# [PASS] test_transferRevertInsufficientBalance() (gas: 16477)
# [PASS] test_transferRevertInvalidReceiver() (gas: 11796)
# [PASS] test_transferRevertInvalidSender() (gas: 11746)
# Test result: ok. 18 passed; 0 failed; 0 skipped; finished in 2.07ms
#
# Ran 1 test suites: 18 tests passed, 0 failed, 0 skipped (18 total tests)

为Berachain合约部署Foundry

现在,代码创建和测试都已完成,接下来创建部署BingBongToken.sol文件所需的脚本。为此,需要将Course.s.sol脚本文件重命名为BingBongToken.s.sol

# FROM: ./create-erc20-contract-using-foundry

mv script/Counter.s.sol script/BingBongToken.s.sol;

然后,添加以下代码以替换现有代码,用于导入钱包私钥和部署合约。

文件位置./script/BingBongToken.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console2} from "forge-std/Script.sol";
import "../src/BingBongToken.sol";

contract BingBongTokenScript is Script {
    /**
     * @dev Relevant source part starts here and spans across multiple lines
     */
    function setUp() public {
    }

    /**
     * @dev Main deployment script
     */
    function run() public {
        // Setup
        uint256 deployerPrivateKey = vm.envUint("WALLET_PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Deploy
        BingBongToken bbt = new BingBongToken("BingBongToken", "BBT", 5678);

        // Verify + End
        console2.log(bbt.totalSupply());
        vm.stopBroadcast();
    }
}

为了验证合约是否能够正确执行,可以通过运行anvil在本地节点上测试。请备份并保存私钥。

终端 1:

# FROM: ./create-erc20-contract-using-foundry

anvil;

# [Expected Output]:
#
#
#                              _   _
#                             (_) | |
#       __ _   _ __   __   __  _  | |
#      / _` | | '_ \  \ \ / / | | | |
#     | (_| | | | | |  \ V /  | | | |
#      \__,_| |_| |_|   \_/   |_| |_|
#
#     0.2.0 (f5b9c02 2023-10-28T00:16:04.060987000Z)
#     https://github.com/foundry-rs/foundry
#
# Available Accounts
# ==================
#
# (0) "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" (10000.000000000000000000 ETH)
# ...
#
# Private Keys
# ==================
#
# (0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# ...

使用上方代码中提供的私钥Private Key,替换.env 文件中的WALLET_PRIVATE_KEY

文件位置./.env

WALLET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

终端 2:

在另一个终端窗口中,运行以下代码,将合约部署到本地节点RPC:

# FROM ./create-erc20-contract-using-foundry

forge script script/BingBongToken.s.sol --fork-url http://localhost:8545 --broadcast;

# [Expected Output]:
# Compiler run successful!
# Script ran successfully.
#
# == Logs ==
#   5678
# ...
# ✅  [Success]Hash: 0xc2b647051d11d8dbd88d131ff268ada417caa27e423747497b624cc3e9c75db8
# Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
# Block: 1
# ...

显示以上结果,部署成功!最后,务必使用ctrl + c停止终端 1中的anvil服务。

部署ERC20合约

部署之前,确保你的钱包里有足够的$BERA代币以支付部署合约所需的费用,并确认已替换.env文件中的WALLET_PRIVATE_KEY

使用本地节点配置,部署到Berachain测试网的流程均相似,但需要指定不同的RPC URL端点:

# FROM ./create-erc20-contract-using-foundry

forge script script/BingBongToken.s.sol --rpc-url https://bartio.rpc.berachain.com/ --broadcast;

# [Expected Output]:
# Compiler run successful!
# Script ran successfully.
#
# == Logs ==
#   5678
# ...
# ✅  [Success]Hash: 0x69aeb8ee5084c44cce00cae2fda3563bd10efb9c8c663ec7b6a6929d6d48a50e
# Contract Address: 0x01870EC5C7656723b31a884259537B183FE15Fa7
# Block: 68764
# ...

验证ERC20合约

目前,在v0.2.0版本的forge中,合约验证存在一些问题,可能导致合约验证无法进行,尝试运行以下代码,应该有助于验证合约:

# FROM ./create-erc20-contract-using-foundry

forge verify-contract 0xYOUR_DEPLOYED_CONTRACT_ADDRESS BingBongToken \
    --etherscan-api-key=xxxxx \
    --watch \
    --constructor-args $(cast abi-encode "constructor(string,string,uint256)" "BingBongToken" "BBT" 5678) \
    --retries=2 \
    --verifier-url=https://api.routescan.io/v2/network/testnet/evm/80084/etherscan/api/;

完整代码库

本节完整代码库,可在Github - Berachain Guides - foundry-erc20中查看。

最后更新于