1657686600
2021 年 12 月,全球最大的加密原生投资公司Paradigm Lab 的 CTO Georgios发布了一篇博客,介绍了一个名为 Foundry 的(基于 evm 的)智能合约开发新框架的发布。
它席卷了 Crypto 社区,并很快成为智能合约开发和测试的行业标准,这在很大程度上要归功于它与其他框架相比的效率。
为了理解 Foundry 的意义,我们需要首先研究它试图解决的问题。
像 Hardhat 和 Truffle 这样的框架存在的主要问题是,它们需要开发人员了解 JavaScript/TypeScript 等脚本语言才能使用该框架。
由于这些脚本语言是 Web 开发的重心,solidity 开发人员不需要了解这些语言来进行智能合约开发,因为它被认为更面向后端。
另一个问题是安全帽本身是使用 TypeScript 实现的,因此它比 Foundry 慢,因为后者是使用 Rust 实现的。
(注意:如果您有兴趣检查基准,请查看此模拟)
Foundry 还有很多很酷的特性,比如:
现在,我希望您对 Foundry 以及使用 Solidity 测试智能合约的必要性有所了解。Foundry 附带了两个开箱即用的惊人 CLI 工具:
让我们开始吧。
安装 Foundry 简单明了。
打开你的终端并运行:
curl -L https://foundry.paradigm.xyz | bash && foundryup
安装 Foundry 后,您可以立即开始使用 Forge 和 Cast。
对于某些操作系统,您可能需要在安装 Foundry 之前安装rust 。
您可以通过运行立即设置 Foundry 项目
伪造初始化 <PROJECT_NAME>
为了让您的生活更轻松,我创建了一个模板存储库,您可以更轻松地开始使用它。它包含所需的库、脚本和目录设置。因此,您只需在终端中运行以下命令:
上面的命令创建了一个名为的新目录foundry-faucet
,并使用我的模板初始化了一个新的 Foundry 项目。这将是目录结构。我们要关注的重要目录和文件是:
我们还应该更新和安装使用的库;为此运行以下命令:
git submodule update --init --recursive
forge install
现在,我们将为我们的 ERC20 代币实现一个水龙头合约,它可以在请求时滴下代币。limit
我们还可以100
通过在合约中设置默认值来限制每个请求的代币数量。
打开src/Faucet.sol
文件并添加以下代码:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
contract Faucet is Ownable {
/// Address of the token that this faucet drips
IERC20 public token;
/// For rate limiting
mapping(address => uint256) public nextRequestAt;
/// Max token limit per request
uint256 public limit = 100;
/// @param _token The address of the faucet's token
constructor(IERC20 _token) {
token = _token;
}
/// Used to send the tokens
/// @param _recipient The address of the tokens recipient
/// @param _amount The amount of tokens required from the faucet
function drip(address _recipient, uint256 _amount) external {
require(_recipient != address(0), "INVALID_RECIPIENT");
require(_amount <= limit, "EXCEEDS_LIMIT");
require(nextRequestAt[_recipient] <= block.timestamp, "TRY_LATER");
nextRequestAt[_recipient] = block.timestamp + (5 minutes);
token.transfer(_recipient, _amount);
}
/// Used to set the max limit per request
/// @dev This method is restricted and should be called only by the owner
/// @param _limit The new limit for the tokens per request
function setLimit(uint256 _limit) external onlyOwner {
limit = _limit;
}
}
我们的水龙头合同已添加。现在我们可以通过运行继续编译合约:
forge build
如果一切顺利,您应该会看到类似的输出:
[⠒] Compiling...
[⠒] Compiling 14 files with 0.8.13
Compiler run successful
甜的!我们已经成功设置了我们的 Foundry 项目并编译了我们的合约,没有任何错误!干得好,安
现在,我们可以开始测试我们的水龙头合约了。
如您所知,与 Hardhat 不同,Forge 帮助我们使用 Solidity 编写单元测试。
如果您打开该src/test/Faucet.t.sol
文件,您将看到一些 utils 和 BaseSetup 合同的导入。
它有一些初始设置,可以初始化一些我们可以在测试中使用的变量。此外,该setUp()
功能类似于beforeEach
安全帽,它在每次测试之前运行。
该setUp()
函数创建两个地址并将它们标记为Alice
和Bob
。当您尝试通过调用跟踪进行调试时,这很有帮助,因为标签与地址一起出现在跟踪中。
(注意:vm.label 被称为作弊码,它是 Forge 特有的;它通过与测试环境中的虚拟机交互来帮助我们做一些特殊的操作。在本文的过程中,我们将看到更多的作弊码。对于完整的作弊码列表,你可以参考这个链接)
将 替换为Faucet.t.sol
以下代码以开始进行单元测试;
// SPDX-License-Identifier: MIT
pragma solidity >=0.8;
import {console} from "forge-std/console.sol";
import {stdStorage, StdStorage, Test} from "forge-std/Test.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {Utils} from "./utils/Utils.sol";
import {Faucet} from "../Faucet.sol";
import {MockERC20} from "../MockERC20.sol";
contract BaseSetup is Test {
Utils internal utils;
Faucet internal faucet;
MockERC20 internal token;
address payable[] internal users;
address internal owner;
address internal dev;
uint256 internal faucetBal = 1000;
function setUp() public virtual {
utils = new Utils();
users = utils.createUsers(2);
owner = users[0];
vm.label(owner, "Owner");
dev = users[1];
vm.label(dev, "Developer");
token = new MockERC20();
faucet = new Faucet(IERC20(token));
token.mint(address(faucet), faucetBal);
}
}
您可以看到我们现在已经创建了新的状态变量,例如faucet
,token
并且我们已经将 and 重命名为和alice
以便bob
于解释。在这种情况下,是从水龙头请求令牌的人,而是水龙头本身的所有者。ownerdevdevowner
在方法的最后三行中setUp()
,我们为水龙头部署了一个模拟令牌,在(水龙头部署)的构造函数中传递其地址new Faucet()
,然后调用并铸造一些令牌到部署的水龙头合约。
现在,我们将继承BaseSetup
合约来为我们的水龙头合约编写单元测试。
在合约下方BaseSetup
,添加以下代码:
contract FaucetTest is BaseSetup {
uint256 amountToDrip = 1;
function setUp() public override {
super.setUp();
}
如前所述,该setUp()
方法在所有测试用例之前运行,这里我们调用setUp()
基础合约的方法,即BaseSetup
合约 via super.setUp()
。
好的,现在让我们开始为我们的合约添加单元测试。setUp()
在 FaucetTest 合约方法的正下方,添加以下代码:
function test_drip_transferToDev() public {
console.log(
"Should transfer tokens to recipient when `drip()` is called"
);
uint256 _inititalDevBal = token.balanceOf(dev);
/// Make sure that initial dev balance is Zero
assertEq(_inititalDevBal, 0);
/// Request some tokens to the dev wallet from the wallet
faucet.drip(dev, amountToDrip);
uint256 _devBalAfterDrip = token.balanceOf(dev);
/// The difference should be equal to the amount requested from the faucet
assertEq(_devBalAfterDrip - _inititalDevBal, amountToDrip);
}
上面的代码帮助我们测试drip()
方法。工作流程很简单。
assertEq(_inititalDevBal, 0);
作用drip()
然后从faucet
合约实例中调用方法dev
后获取余额drip()
dev
之前和之后的账户余额差drip()
应该等于amountToDrip
,作为状态变量存储在 FaucetTest 合约中现在,让我们保存文件并运行测试:.forge test
您应该会在终端中看到类似于以下内容的输出:
凉爽的!让我们添加更多测试。
上面的测试验证该drip()
方法将令牌传输到dev
. 因此,我们还应该检查转账是否有效,这意味着水龙头的代币余额应该减少。
在下面添加以下测试 -test_drip_transferToDev()
方法。
function test_drip_reduceFaucetBalance() public {
console.log("The faucet balance should be reduced");
faucet.drip(dev, amountToDrip);
assertEq(token.balanceOf(address(faucet)), faucetBal - amountToDrip);
}
这确保了开发人员收到的代币实际上是从水龙头发出的——如果是这样,水龙头的余额应该减少。
我们可以通过再次运行测试套件来确保:forge test
如果一切顺利,那么您的输出应该类似于以下内容:
甜的!如果您注意到,我们console.log
的测试用例中有语句,但它们没有显示在控制台中。原因是 Forge 默认不显示日志。要显示日志,我们需要运行详细程度为 2 的命令:forge test -vv
将显示日志。
此外,如果您的合约发出了任何事件,您可以在测试中以详细程度三(-vvv)查看它们。您可以获得详细的测试调用跟踪,详细程度高达 5 级,这有助于更好地调试。
好吧,让我们继续添加更多测试。现在我们将测试我们的速率限制机制。drip()
在使用相同的收件人地址拨打电话之前,应该至少有五分钟的间隔。
function test_drip_revertIfThrottled() public {
console.log("Should revert if tried to throttle");
faucet.drip(dev, amountToDrip);
vm.expectRevert(abi.encodePacked("TRY_LATER"));
faucet.drip(dev, amountToDrip);
}
vm.expectRevert(bytes32)
是另一个作弊码,用于检查下一个调用是否返回给定的错误消息。在这种情况下,错误消息是TRY_LATER
。它接受错误消息作为字节而不是字符串,因此我们使用abi.encodePacked
.
如果你还记得的话,我提到过 Forge 附带了一个开箱即用的模糊器。试一试吧。
我们将测试test_drip_transferToDev
和结合起来test_drip_reduceFaucetBalance
,而不是手动传递输入,我们将允许模糊器输入值,以便我们可以确保我们的合约处理不同的输入。
function test_drip_withFuzzing(address _recipient, uint256 _amount) public {
console.log("Should handle fuzzing");
/// inform the constraints to the fuzzer, so that the tests don't revert on bad inputs.
vm.assume(_amount <= 100);
vm.assume(_recipient != address(0));
uint256 _inititalBal = token.balanceOf(_recipient);
faucet.drip(_recipient, _amount);
uint256 _balAfterDrip = token.balanceOf(_recipient);
assertEq(_balAfterDrip - _inititalBal, _amount);
assertEq(token.balanceOf(address(faucet)), faucetBal - _amount);
}
Fuzzing 是基于属性的测试。Forge 将对任何需要至少一个参数的测试应用模糊测试。
执行测试套件时,您可以在输出中找到以下行:
[通过] test_drip_withFuzzing(地址,uint256)(运行:256)
从上面的输出我们可以推断出 Forge fuzzer 在256
随机输入的情况下调用了 test_drip_withFuzzing() 方法。FOUNDRY_FUZZ_RUNS
但是,我们可以使用环境变量覆盖这个数字。
现在,让我们为 owner-only 方法添加更多测试setLimit()
function test_setLimit() public {
console.log("Should set the limit when called by the owner");
faucet.setLimit(1000);
/// the limit should be updated assertEq(faucet.limit(), 1000); } function test_setLimit_revertIfNotOwner() public {console.log("Should revert if not called by Owner"); /// Sets the msg.sender as dev for the next tx vm.prank(dev); vm.expectRevert(abi.encodePacked("Ownable: caller is not the owner")); faucet.setLimit(1000); }
在该test_setLimit_revertIfNotOwner()
方法中,使用了新的作弊码vm.prank(address)
。它通过使用给定地址覆盖 msg.sender 来恶作剧虚拟机;在我们的例子中是dev
. 因此,当我们的水龙头合约继承了合约时,setLimit()
应该与消息一起恢复。caller is not the ownerOwnable
好的,让我们通过再次运行确保没有测试失败forge test
。
Sweet现在是部署的时候了。
从文件创建一个新文件.env.example
并将其命名为.env
. 请填写您的 INFURA_API_KEY 和 PRIVATE_KEY(使用 Kovan 测试网资金)。
填充所有字段后,您就可以部署到 Kovan。在部署水龙头之前,我们需要部署我们的 ERC20 代币。
您可以在目录中找到部署脚本,并通过执行bash 脚本scripts
将 MockERC20 令牌部署到 Kovan 测试网。./scripts/deploy_token_kovan.sh
输出看起来像这样:
Deployer: (YOUR_DEPLOYMENT_ADDRESS)
Deployed to: 0x1a70d8a2a02c9cf0faa5551304ba770b5496ed80
Transaction hash: 0xa3780d2e3e1d1f9346035144f3c2d62f31918b613a370f416a4fb1a6c2eadc77
为确保交易真正通过,您可以在https://kovan.etherscan.io中搜索交易哈希
复制Deployed to:
地址,因为它是我们应该用于部署 Faucet 合约的 MockERC20 代币的地址。要部署水龙头,您可以执行./scripts/deploy_faucet_kovan.sh
脚本。
它将提示您输入令牌地址;然后输入之前部署的复制的 MockERC20 令牌地址。
输出应如下所示:
Woohoo我们已经使用 Forge 成功编译、测试并部署了我们的合约到 Kovan 测试网
我们仍然需要在 Etherscan 上验证合约,并为 Faucet 铸造一些 MockERC20 代币(您可以为此使用 cast!)以使其按预期工作。我会把这个留给你们作为练习,让你们自己试试!
与往常一样,您可以在此处找到本文的 GitHub 存储库。
在本文中,我们只介绍了 Forge 的几块。Foundry 是一个非常强大的智能合约框架,它也在快速发展。
还有更多很酷的功能,例如代码覆盖、合约验证、gas 快照、调用跟踪和交互式调试。通过测试更多功能,随意使用 repo。快乐编码
来源:https ://blog.logrocket.com/unit-testing-smart-contracts-forge/
1657686600
2021 年 12 月,全球最大的加密原生投资公司Paradigm Lab 的 CTO Georgios发布了一篇博客,介绍了一个名为 Foundry 的(基于 evm 的)智能合约开发新框架的发布。
它席卷了 Crypto 社区,并很快成为智能合约开发和测试的行业标准,这在很大程度上要归功于它与其他框架相比的效率。
为了理解 Foundry 的意义,我们需要首先研究它试图解决的问题。
像 Hardhat 和 Truffle 这样的框架存在的主要问题是,它们需要开发人员了解 JavaScript/TypeScript 等脚本语言才能使用该框架。
由于这些脚本语言是 Web 开发的重心,solidity 开发人员不需要了解这些语言来进行智能合约开发,因为它被认为更面向后端。
另一个问题是安全帽本身是使用 TypeScript 实现的,因此它比 Foundry 慢,因为后者是使用 Rust 实现的。
(注意:如果您有兴趣检查基准,请查看此模拟)
Foundry 还有很多很酷的特性,比如:
现在,我希望您对 Foundry 以及使用 Solidity 测试智能合约的必要性有所了解。Foundry 附带了两个开箱即用的惊人 CLI 工具:
让我们开始吧。
安装 Foundry 简单明了。
打开你的终端并运行:
curl -L https://foundry.paradigm.xyz | bash && foundryup
安装 Foundry 后,您可以立即开始使用 Forge 和 Cast。
对于某些操作系统,您可能需要在安装 Foundry 之前安装rust 。
您可以通过运行立即设置 Foundry 项目
伪造初始化 <PROJECT_NAME>
为了让您的生活更轻松,我创建了一个模板存储库,您可以更轻松地开始使用它。它包含所需的库、脚本和目录设置。因此,您只需在终端中运行以下命令:
上面的命令创建了一个名为的新目录foundry-faucet
,并使用我的模板初始化了一个新的 Foundry 项目。这将是目录结构。我们要关注的重要目录和文件是:
我们还应该更新和安装使用的库;为此运行以下命令:
git submodule update --init --recursive
forge install
现在,我们将为我们的 ERC20 代币实现一个水龙头合约,它可以在请求时滴下代币。limit
我们还可以100
通过在合约中设置默认值来限制每个请求的代币数量。
打开src/Faucet.sol
文件并添加以下代码:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
contract Faucet is Ownable {
/// Address of the token that this faucet drips
IERC20 public token;
/// For rate limiting
mapping(address => uint256) public nextRequestAt;
/// Max token limit per request
uint256 public limit = 100;
/// @param _token The address of the faucet's token
constructor(IERC20 _token) {
token = _token;
}
/// Used to send the tokens
/// @param _recipient The address of the tokens recipient
/// @param _amount The amount of tokens required from the faucet
function drip(address _recipient, uint256 _amount) external {
require(_recipient != address(0), "INVALID_RECIPIENT");
require(_amount <= limit, "EXCEEDS_LIMIT");
require(nextRequestAt[_recipient] <= block.timestamp, "TRY_LATER");
nextRequestAt[_recipient] = block.timestamp + (5 minutes);
token.transfer(_recipient, _amount);
}
/// Used to set the max limit per request
/// @dev This method is restricted and should be called only by the owner
/// @param _limit The new limit for the tokens per request
function setLimit(uint256 _limit) external onlyOwner {
limit = _limit;
}
}
我们的水龙头合同已添加。现在我们可以通过运行继续编译合约:
forge build
如果一切顺利,您应该会看到类似的输出:
[⠒] Compiling...
[⠒] Compiling 14 files with 0.8.13
Compiler run successful
甜的!我们已经成功设置了我们的 Foundry 项目并编译了我们的合约,没有任何错误!干得好,安
现在,我们可以开始测试我们的水龙头合约了。
如您所知,与 Hardhat 不同,Forge 帮助我们使用 Solidity 编写单元测试。
如果您打开该src/test/Faucet.t.sol
文件,您将看到一些 utils 和 BaseSetup 合同的导入。
它有一些初始设置,可以初始化一些我们可以在测试中使用的变量。此外,该setUp()
功能类似于beforeEach
安全帽,它在每次测试之前运行。
该setUp()
函数创建两个地址并将它们标记为Alice
和Bob
。当您尝试通过调用跟踪进行调试时,这很有帮助,因为标签与地址一起出现在跟踪中。
(注意:vm.label 被称为作弊码,它是 Forge 特有的;它通过与测试环境中的虚拟机交互来帮助我们做一些特殊的操作。在本文的过程中,我们将看到更多的作弊码。对于完整的作弊码列表,你可以参考这个链接)
将 替换为Faucet.t.sol
以下代码以开始进行单元测试;
// SPDX-License-Identifier: MIT
pragma solidity >=0.8;
import {console} from "forge-std/console.sol";
import {stdStorage, StdStorage, Test} from "forge-std/Test.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {Utils} from "./utils/Utils.sol";
import {Faucet} from "../Faucet.sol";
import {MockERC20} from "../MockERC20.sol";
contract BaseSetup is Test {
Utils internal utils;
Faucet internal faucet;
MockERC20 internal token;
address payable[] internal users;
address internal owner;
address internal dev;
uint256 internal faucetBal = 1000;
function setUp() public virtual {
utils = new Utils();
users = utils.createUsers(2);
owner = users[0];
vm.label(owner, "Owner");
dev = users[1];
vm.label(dev, "Developer");
token = new MockERC20();
faucet = new Faucet(IERC20(token));
token.mint(address(faucet), faucetBal);
}
}
您可以看到我们现在已经创建了新的状态变量,例如faucet
,token
并且我们已经将 and 重命名为和alice
以便bob
于解释。在这种情况下,是从水龙头请求令牌的人,而是水龙头本身的所有者。ownerdevdevowner
在方法的最后三行中setUp()
,我们为水龙头部署了一个模拟令牌,在(水龙头部署)的构造函数中传递其地址new Faucet()
,然后调用并铸造一些令牌到部署的水龙头合约。
现在,我们将继承BaseSetup
合约来为我们的水龙头合约编写单元测试。
在合约下方BaseSetup
,添加以下代码:
contract FaucetTest is BaseSetup {
uint256 amountToDrip = 1;
function setUp() public override {
super.setUp();
}
如前所述,该setUp()
方法在所有测试用例之前运行,这里我们调用setUp()
基础合约的方法,即BaseSetup
合约 via super.setUp()
。
好的,现在让我们开始为我们的合约添加单元测试。setUp()
在 FaucetTest 合约方法的正下方,添加以下代码:
function test_drip_transferToDev() public {
console.log(
"Should transfer tokens to recipient when `drip()` is called"
);
uint256 _inititalDevBal = token.balanceOf(dev);
/// Make sure that initial dev balance is Zero
assertEq(_inititalDevBal, 0);
/// Request some tokens to the dev wallet from the wallet
faucet.drip(dev, amountToDrip);
uint256 _devBalAfterDrip = token.balanceOf(dev);
/// The difference should be equal to the amount requested from the faucet
assertEq(_devBalAfterDrip - _inititalDevBal, amountToDrip);
}
上面的代码帮助我们测试drip()
方法。工作流程很简单。
assertEq(_inititalDevBal, 0);
作用drip()
然后从faucet
合约实例中调用方法dev
后获取余额drip()
dev
之前和之后的账户余额差drip()
应该等于amountToDrip
,作为状态变量存储在 FaucetTest 合约中现在,让我们保存文件并运行测试:.forge test
您应该会在终端中看到类似于以下内容的输出:
凉爽的!让我们添加更多测试。
上面的测试验证该drip()
方法将令牌传输到dev
. 因此,我们还应该检查转账是否有效,这意味着水龙头的代币余额应该减少。
在下面添加以下测试 -test_drip_transferToDev()
方法。
function test_drip_reduceFaucetBalance() public {
console.log("The faucet balance should be reduced");
faucet.drip(dev, amountToDrip);
assertEq(token.balanceOf(address(faucet)), faucetBal - amountToDrip);
}
这确保了开发人员收到的代币实际上是从水龙头发出的——如果是这样,水龙头的余额应该减少。
我们可以通过再次运行测试套件来确保:forge test
如果一切顺利,那么您的输出应该类似于以下内容:
甜的!如果您注意到,我们console.log
的测试用例中有语句,但它们没有显示在控制台中。原因是 Forge 默认不显示日志。要显示日志,我们需要运行详细程度为 2 的命令:forge test -vv
将显示日志。
此外,如果您的合约发出了任何事件,您可以在测试中以详细程度三(-vvv)查看它们。您可以获得详细的测试调用跟踪,详细程度高达 5 级,这有助于更好地调试。
好吧,让我们继续添加更多测试。现在我们将测试我们的速率限制机制。drip()
在使用相同的收件人地址拨打电话之前,应该至少有五分钟的间隔。
function test_drip_revertIfThrottled() public {
console.log("Should revert if tried to throttle");
faucet.drip(dev, amountToDrip);
vm.expectRevert(abi.encodePacked("TRY_LATER"));
faucet.drip(dev, amountToDrip);
}
vm.expectRevert(bytes32)
是另一个作弊码,用于检查下一个调用是否返回给定的错误消息。在这种情况下,错误消息是TRY_LATER
。它接受错误消息作为字节而不是字符串,因此我们使用abi.encodePacked
.
如果你还记得的话,我提到过 Forge 附带了一个开箱即用的模糊器。试一试吧。
我们将测试test_drip_transferToDev
和结合起来test_drip_reduceFaucetBalance
,而不是手动传递输入,我们将允许模糊器输入值,以便我们可以确保我们的合约处理不同的输入。
function test_drip_withFuzzing(address _recipient, uint256 _amount) public {
console.log("Should handle fuzzing");
/// inform the constraints to the fuzzer, so that the tests don't revert on bad inputs.
vm.assume(_amount <= 100);
vm.assume(_recipient != address(0));
uint256 _inititalBal = token.balanceOf(_recipient);
faucet.drip(_recipient, _amount);
uint256 _balAfterDrip = token.balanceOf(_recipient);
assertEq(_balAfterDrip - _inititalBal, _amount);
assertEq(token.balanceOf(address(faucet)), faucetBal - _amount);
}
Fuzzing 是基于属性的测试。Forge 将对任何需要至少一个参数的测试应用模糊测试。
执行测试套件时,您可以在输出中找到以下行:
[通过] test_drip_withFuzzing(地址,uint256)(运行:256)
从上面的输出我们可以推断出 Forge fuzzer 在256
随机输入的情况下调用了 test_drip_withFuzzing() 方法。FOUNDRY_FUZZ_RUNS
但是,我们可以使用环境变量覆盖这个数字。
现在,让我们为 owner-only 方法添加更多测试setLimit()
function test_setLimit() public {
console.log("Should set the limit when called by the owner");
faucet.setLimit(1000);
/// the limit should be updated assertEq(faucet.limit(), 1000); } function test_setLimit_revertIfNotOwner() public {console.log("Should revert if not called by Owner"); /// Sets the msg.sender as dev for the next tx vm.prank(dev); vm.expectRevert(abi.encodePacked("Ownable: caller is not the owner")); faucet.setLimit(1000); }
在该test_setLimit_revertIfNotOwner()
方法中,使用了新的作弊码vm.prank(address)
。它通过使用给定地址覆盖 msg.sender 来恶作剧虚拟机;在我们的例子中是dev
. 因此,当我们的水龙头合约继承了合约时,setLimit()
应该与消息一起恢复。caller is not the ownerOwnable
好的,让我们通过再次运行确保没有测试失败forge test
。
Sweet现在是部署的时候了。
从文件创建一个新文件.env.example
并将其命名为.env
. 请填写您的 INFURA_API_KEY 和 PRIVATE_KEY(使用 Kovan 测试网资金)。
填充所有字段后,您就可以部署到 Kovan。在部署水龙头之前,我们需要部署我们的 ERC20 代币。
您可以在目录中找到部署脚本,并通过执行bash 脚本scripts
将 MockERC20 令牌部署到 Kovan 测试网。./scripts/deploy_token_kovan.sh
输出看起来像这样:
Deployer: (YOUR_DEPLOYMENT_ADDRESS)
Deployed to: 0x1a70d8a2a02c9cf0faa5551304ba770b5496ed80
Transaction hash: 0xa3780d2e3e1d1f9346035144f3c2d62f31918b613a370f416a4fb1a6c2eadc77
为确保交易真正通过,您可以在https://kovan.etherscan.io中搜索交易哈希
复制Deployed to:
地址,因为它是我们应该用于部署 Faucet 合约的 MockERC20 代币的地址。要部署水龙头,您可以执行./scripts/deploy_faucet_kovan.sh
脚本。
它将提示您输入令牌地址;然后输入之前部署的复制的 MockERC20 令牌地址。
输出应如下所示:
Woohoo我们已经使用 Forge 成功编译、测试并部署了我们的合约到 Kovan 测试网
我们仍然需要在 Etherscan 上验证合约,并为 Faucet 铸造一些 MockERC20 代币(您可以为此使用 cast!)以使其按预期工作。我会把这个留给你们作为练习,让你们自己试试!
与往常一样,您可以在此处找到本文的 GitHub 存储库。
在本文中,我们只介绍了 Forge 的几块。Foundry 是一个非常强大的智能合约框架,它也在快速发展。
还有更多很酷的功能,例如代码覆盖、合约验证、gas 快照、调用跟踪和交互式调试。通过测试更多功能,随意使用 repo。快乐编码
来源:https ://blog.logrocket.com/unit-testing-smart-contracts-forge/
1633719600
One of Forge's killer features is that it can provision servers with your choice of database: MySQL, MariaDB or PostgreSQL. Once the server has provisioned you can then use Forge to:
1657653240
In December 2021, the world’s largest crypto-native investment firm, Paradigm Lab’s CTO Georgios released a blog regarding the release of a new framework for (evm-based) smart contract development, called Foundry.
It took the Crypto community by storm, and soon became the industry standard for development and testing of smart contracts, owing much to its efficiency in comparison to other frameworks.
In order to understand the significance of Foundry, we need to first look into the problems it tries to solve.
The main problem that lies with frameworks like Hardhat and Truffle is that they require the developers to know a scripting language like JavaScript/TypeScript in order to work with the framework.
See more at: https://blog.logrocket.com/unit-testing-smart-contracts-forge/
1657685160
En diciembre de 2021, Georgios, el director de tecnología de Paradigm Lab , la firma de inversión nativa de criptomonedas más grande del mundo, publicó un blog sobre el lanzamiento de un nuevo marco para el desarrollo de contratos inteligentes (basado en evm), llamado Foundry.
Tomó por asalto a la comunidad Crypto y pronto se convirtió en el estándar de la industria para el desarrollo y prueba de contratos inteligentes, debido en gran parte a su eficiencia en comparación con otros marcos.
Para comprender la importancia de Foundry, primero debemos analizar los problemas que intenta resolver.
El problema principal que se encuentra con marcos como Hardhat y Truffle es que requieren que los desarrolladores conozcan un lenguaje de secuencias de comandos como JavaScript/TypeScript para poder trabajar con el marco.
Como esos lenguajes de secuencias de comandos son pesados para el desarrollo web, el desarrollador de solidez no necesita conocer dichos lenguajes para el desarrollo de contratos inteligentes, ya que se considera más orientado al back-end.
Otro problema es que el casco en sí se implementa con TypeScript, por lo que es más lento que Foundry, ya que este último se implementa con Rust.
(Nota: si está interesado en verificar los puntos de referencia, eche un vistazo a esta simulación )
Foundry tiene muchas características geniales además de esto como:
Ahora, espero que tenga una descripción general de Foundry y la necesidad de probar contratos inteligentes usando Solidity. Foundry se envía con dos increíbles herramientas CLI listas para usar:
Empecemos.
La instalación de Foundry es simple y directa.
Abre tu terminal y ejecuta:
curl -L https://foundry.paradigm.xyz | bash && foundryup
Una vez que Foundry está instalado, puede comenzar a usar Forge y Cast de inmediato.
Para algunos sistemas operativos, es posible que desee instalar Rust antes de instalar Foundry.
Puede configurar instantáneamente un proyecto de Foundry de inmediato ejecutando
Forge init <NOMBRE_DE_PROYECTO>
Para hacerte la vida más fácil, he creado un repositorio de plantillas , con el que puedes empezar más fácilmente. Contiene las bibliotecas, los scripts y la configuración del directorio necesarios. Entonces, todo lo que necesita hacer es ejecutar el siguiente comando en su terminal:
El comando anterior crea un nuevo directorio llamado foundry-faucet
e inicializa un nuevo proyecto de Foundry utilizando mi plantilla. Esta sería la estructura del directorio. Los directorios y archivos importantes en los que queremos centrarnos son:
También debemos actualizar e instalar las bibliotecas utilizadas; para eso ejecuta los siguientes comandos:
git submodule update --init --recursive
forge install
Ahora, vamos a implementar un contrato faucet para nuestro token ERC20 que puede gotear tokens cuando se solicite. También podemos restringir la cantidad de tokens por solicitud configurando una limit
que estará 100
por defecto en nuestro contrato.
Abra el src/Faucet.sol
archivo y agregue el siguiente código:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
contract Faucet is Ownable {
/// Address of the token that this faucet drips
IERC20 public token;
/// For rate limiting
mapping(address => uint256) public nextRequestAt;
/// Max token limit per request
uint256 public limit = 100;
/// @param _token The address of the faucet's token
constructor(IERC20 _token) {
token = _token;
}
/// Used to send the tokens
/// @param _recipient The address of the tokens recipient
/// @param _amount The amount of tokens required from the faucet
function drip(address _recipient, uint256 _amount) external {
require(_recipient != address(0), "INVALID_RECIPIENT");
require(_amount <= limit, "EXCEEDS_LIMIT");
require(nextRequestAt[_recipient] <= block.timestamp, "TRY_LATER");
nextRequestAt[_recipient] = block.timestamp + (5 minutes);
token.transfer(_recipient, _amount);
}
/// Used to set the max limit per request
/// @dev This method is restricted and should be called only by the owner
/// @param _limit The new limit for the tokens per request
function setLimit(uint256 _limit) external onlyOwner {
limit = _limit;
}
}
Se ha agregado nuestro contrato de llave. Ahora podemos continuar y compilar los contratos ejecutando:
forge build
Si todo va bien, debería ver un resultado similar:
[⠒] Compiling...
[⠒] Compiling 14 files with 0.8.13
Compiler run successful
¡Dulce! ¡Hemos configurado con éxito nuestro proyecto Foundry y hemos compilado nuestro contrato sin ningún error! buen trabajo anónimo
Ahora, podemos continuar y comenzar a probar nuestro contrato Faucet.
Como saben, a diferencia de Hardhat, Forge nos ayuda a escribir pruebas unitarias usando Solidity.
Si abre el src/test/Faucet.t.sol
archivo, ya verá algunas importaciones de utilidades y un contrato BaseSetup.
Tiene una configuración inicial que inicializa algunas variables que podemos usar en nuestras pruebas. Además, la setUp()
función es similar a la beforeEach
del casco y se ejecuta antes de cada prueba.
La setUp()
función crea dos direcciones y las etiqueta Alice
como Bob
. Es útil cuando intenta depurar a través de seguimientos de llamadas, ya que la etiqueta aparece en los seguimientos junto con la dirección.
(Nota: vm.label se denomina código de trucos y es específico de Forge; nos ayuda a realizar algunas operaciones especiales interactuando con la máquina virtual en el entorno de prueba. Veremos más códigos de trucos durante el transcurso del artículo. Para la lista completa de códigos de trucos, puede consultar este enlace )
Reemplace Faucet.t.sol
con el siguiente código para comenzar con las pruebas unitarias;
// SPDX-License-Identifier: MIT
pragma solidity >=0.8;
import {console} from "forge-std/console.sol";
import {stdStorage, StdStorage, Test} from "forge-std/Test.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {Utils} from "./utils/Utils.sol";
import {Faucet} from "../Faucet.sol";
import {MockERC20} from "../MockERC20.sol";
contract BaseSetup is Test {
Utils internal utils;
Faucet internal faucet;
MockERC20 internal token;
address payable[] internal users;
address internal owner;
address internal dev;
uint256 internal faucetBal = 1000;
function setUp() public virtual {
utils = new Utils();
users = utils.createUsers(2);
owner = users[0];
vm.label(owner, "Owner");
dev = users[1];
vm.label(dev, "Developer");
token = new MockERC20();
faucet = new Faucet(IERC20(token));
token.mint(address(faucet), faucetBal);
}
}
Puede ver que ahora hemos creado nuevas variables de estado como faucet
, token
y también hemos renombrado alice
y bob
a owner
y dev
para una fácil interpretación. En este contexto, dev
es alguien que solicita fichas de la llave mientras que owner
es el propietario de la llave.
En las últimas tres líneas del setUp()
método, implementamos un token simulado para el grifo, pasamos su dirección en el constructor de new Faucet()
(implementación del grifo) y luego llamamos y acuñamos algunos tokens para el contrato del grifo implementado.
Ahora, heredaremos el BaseSetup
contrato para escribir pruebas unitarias para nuestro contrato Faucet.
Debajo del BaseSetup
contrato, agregue el siguiente código:
contract FaucetTest is BaseSetup {
uint256 amountToDrip = 1;
function setUp() public override {
super.setUp();
}
Como se mencionó anteriormente, el setUp()
método se ejecuta antes de todos los casos de prueba y aquí llamamos al setUp()
método del contrato base, que es el BaseSetup
contrato a través de super.setUp()
.
Muy bien, ahora comencemos a agregar pruebas unitarias para nuestro contrato. Justo debajo del setUp()
método del contrato FaucetTest, agregue el siguiente fragmento de código:
function test_drip_transferToDev() public {
console.log(
"Should transfer tokens to recipient when `drip()` is called"
);
uint256 _inititalDevBal = token.balanceOf(dev);
/// Make sure that initial dev balance is Zero
assertEq(_inititalDevBal, 0);
/// Request some tokens to the dev wallet from the wallet
faucet.drip(dev, amountToDrip);
uint256 _devBalAfterDrip = token.balanceOf(dev);
/// The difference should be equal to the amount requested from the faucet
assertEq(_devBalAfterDrip - _inititalDevBal, amountToDrip);
}
El código anterior nos ayuda a probar el drip()
método. El flujo de trabajo es simple.
assertEq(_inititalDevBal, 0);
hace la línea.drip()
método desde la faucet
instancia del contrato.dev
después de que drip()
se llamedev
cuenta antes y después de drip()
debe ser igual a amountToDrip
, que se almacena como una variable de estado en el contrato FaucetTestAhora, guardemos el archivo y ejecutemos la prueba: .forge test
Debería ver la salida en su terminal algo similar a esto:
¡Enfriar! Agreguemos algunas pruebas más.
La prueba anterior verifica que el drip()
método transfiere los tokens al dev
. Por lo tanto, también debemos verificar que la transferencia sea válida, lo que significa que el saldo de fichas del faucet debe reducirse.
Agregue la siguiente prueba a continuación: el test_drip_transferToDev()
método.
function test_drip_reduceFaucetBalance() public {
console.log("The faucet balance should be reduced");
faucet.drip(dev, amountToDrip);
assertEq(token.balanceOf(address(faucet)), faucetBal - amountToDrip);
}
Esto asegura que los tokens que recibió el desarrollador se envíen realmente desde el faucet; si es así, el saldo del faucet debe reducirse.
Podemos asegurarnos ejecutando el conjunto de pruebas nuevamente:forge test
Si todo va bien, su resultado debería ser similar a este:
¡Dulce! Si lo ha notado, tenemos console.log
declaraciones en nuestros casos de prueba, pero no aparecen en la consola. El motivo es que Forge no muestra los registros de forma predeterminada. Para que se muestren los registros, debemos ejecutar el comando con verbosidad 2: forge test -vv
mostrará los registros.
Además, si hay eventos emitidos por su contrato, puede verlos en las pruebas con verbosidad tres (-vvv). Puede obtener un seguimiento de llamadas detallado para sus pruebas con un nivel de detalle cinco, lo que ayuda a mejorar la depuración.
Muy bien, sigamos agregando más pruebas. Ahora vamos a probar nuestro mecanismo de límite de tasa. Debe haber al menos un intervalo de cinco minutos antes de llamar drip()
con la misma dirección del destinatario.
function test_drip_revertIfThrottled() public {
console.log("Should revert if tried to throttle");
faucet.drip(dev, amountToDrip);
vm.expectRevert(abi.encodePacked("TRY_LATER"));
faucet.drip(dev, amountToDrip);
}
vm.expectRevert(bytes32)
es otro código de trucos que comprueba si la próxima llamada vuelve con el mensaje de error dado. En este caso, el mensaje de error es TRY_LATER
. Acepta el mensaje de error como bytes, no como una cadena, por lo que estamos usando abi.encodePacked
.
Si recuerdas, mencioné que Forge se envía con un fuzzer listo para usar. Hagamos un intento.
Combinamos las pruebas test_drip_transferToDev
y test_drip_reduceFaucetBalance
, y en lugar de pasar las entradas manualmente, permitiríamos que el fuzzer ingrese los valores para que podamos asegurarnos de que nuestro contrato maneja diferentes entradas.
function test_drip_withFuzzing(address _recipient, uint256 _amount) public {
console.log("Should handle fuzzing");
/// inform the constraints to the fuzzer, so that the tests don't revert on bad inputs.
vm.assume(_amount <= 100);
vm.assume(_recipient != address(0));
uint256 _inititalBal = token.balanceOf(_recipient);
faucet.drip(_recipient, _amount);
uint256 _balAfterDrip = token.balanceOf(_recipient);
assertEq(_balAfterDrip - _inititalBal, _amount);
assertEq(token.balanceOf(address(faucet)), faucetBal - _amount);
}
Fuzzing es una prueba basada en propiedades. Forge aplicará fuzzing a cualquier prueba que tome al menos un parámetro.
Cuando ejecuta el conjunto de pruebas, puede encontrar la siguiente línea en la salida:
[PASS] test_drip_withFuzzing(dirección,uint256) (se ejecuta: 256)
Del resultado anterior podemos inferir que el fuzzer de Forge llamó al método test_drip_withFuzzing() 256
veces con entradas aleatorias. Sin embargo, podemos anular este número usando la FOUNDRY_FUZZ_RUNS
variable de entorno.
Ahora, agreguemos un par de pruebas más para el método de solo propietario.setLimit()
function test_setLimit() public {
console.log("Should set the limit when called by the owner");
faucet.setLimit(1000);
/// the limit should be updated assertEq(faucet.limit(), 1000); } function test_setLimit_revertIfNotOwner() public {console.log("Should revert if not called by Owner"); /// Sets the msg.sender as dev for the next tx vm.prank(dev); vm.expectRevert(abi.encodePacked("Ownable: caller is not the owner")); faucet.setLimit(1000); }
En el test_setLimit_revertIfNotOwner()
método, vm.prank(address)
se utiliza un nuevo código de trucos. Bromea con la máquina virtual anulando msg.sender con la dirección dada; en nuestro caso es dev
. Entonces, setLimit()
debería revertirse con el caller is not the owner
mensaje ya que nuestro contrato Faucet hereda el Ownable
contrato.
De acuerdo, asegurémonos de que ninguna prueba falle ejecutándola de forge test
nuevo.
DulceAhora es el momento de la implementación.
Cree un nuevo archivo a partir de un .env.example
archivo y asígnele el nombre .env
. Complete su INFURA_API_KEY y PRIVATE_KEY (con fondos de prueba de Kovan).
Una vez que todos los campos estén completos, estará listo para la implementación en Kovan. Antes de implementar el faucet, debemos implementar nuestro token ERC20.
Puede encontrar los scripts de implementación dentro del scripts
directorio e implementar el token MockERC20 en la red de prueba de Kovan ejecutando el ./scripts/deploy_token_kovan.sh
script bash.
La salida sería algo como esto:
Deployer: (YOUR_DEPLOYMENT_ADDRESS)
Deployed to: 0x1a70d8a2a02c9cf0faa5551304ba770b5496ed80
Transaction hash: 0xa3780d2e3e1d1f9346035144f3c2d62f31918b613a370f416a4fb1a6c2eadc77
Para asegurarse de que la transacción realmente se realizó, puede buscar el hash de la transacción en https://kovan.etherscan.io
Copie la Deployed to:
dirección, ya que es la dirección del token MockERC20 que debemos usar para implementar nuestro contrato Faucet. Para implementar el faucet, puede ejecutar el ./scripts/deploy_faucet_kovan.sh
script.
Le pedirá que ingrese la dirección del token; luego ingrese la dirección del token MockERC20 copiado que se implementó anteriormente.
La salida debería ser algo como esto:
WoohooHemos compilado, probado e implementado con éxito nuestro contrato en la red de prueba de Kovan usando Forge
Todavía tenemos que verificar el contrato en Etherscan y también acuñar algunos tokens MockERC20 en Faucet (¡puedes usar cast para esto!) para que funcione según lo previsto. ¡Os dejo esto como ejercicio para que lo probéis vosotros mismos!
Como siempre, puede encontrar el repositorio de GitHub para este artículo aquí .
En este artículo solo cubrimos algunas piezas de Forge. Foundry es un marco muy poderoso para contratos inteligentes y también se está desarrollando rápidamente.
Hay más funciones interesantes como cobertura de código, verificación de contrato, instantáneas de gas, seguimiento de llamadas y depuración interactiva. Siéntase libre de jugar con el repositorio probando más funciones. Codificación feliz
Fuente: https://blog.logrocket.com/unit-testing-smart-contracts-forge/
1657677600
En décembre 2021, la plus grande société d'investissement crypto-native au monde, le directeur technique de Paradigm Lab, Georgios, a publié un blog concernant la publication d'un nouveau cadre pour le développement de contrats intelligents (basé sur evm), appelé Foundry.
Il a pris d'assaut la communauté Crypto et est rapidement devenu la norme de l'industrie pour le développement et le test de contrats intelligents, en grande partie grâce à son efficacité par rapport à d'autres frameworks.
Afin de comprendre l'importance de Foundry, nous devons d'abord examiner les problèmes qu'il tente de résoudre.
Le principal problème des frameworks comme Hardhat et Truffle est qu'ils exigent que les développeurs connaissent un langage de script comme JavaScript/TypeScript afin de travailler avec le framework.
Comme ces langages de script sont lourds pour le développement Web, le développeur Solidity n'a pas besoin de connaître ces langages pour le développement de contrats intelligents, car ils sont considérés comme plus orientés vers le backend.
Un autre problème est que le casque lui-même est implémenté à l'aide de TypeScript, il est donc plus lent que Foundry car ce dernier est implémenté à l'aide de Rust.
(Remarque : si vous souhaitez vérifier les points de repère, veuillez consulter cette simulation )
Foundry a beaucoup de fonctionnalités intéressantes à part cela comme :
Maintenant, j'espère que vous avez un aperçu de Foundry et de la nécessité de tester des contrats intelligents à l'aide de Solidity. Foundry est livré avec deux incroyables outils CLI prêts à l'emploi :
Commençons.
L'installation de Foundry est simple et directe.
Ouvrez votre terminal et exécutez :
curl -L https://foundry.paradigm.xyz | bash && foundryup
Une fois Foundry installé, vous pouvez commencer à utiliser Forge et Cast immédiatement.
Pour certains systèmes d'exploitation, vous souhaiterez peut-être installer Rust avant d'installer Foundry.
Vous pouvez configurer instantanément un projet Foundry en exécutant
forge init <PROJECT_NAME>
Pour vous faciliter la vie, j'ai créé un référentiel de modèles , avec lequel vous pouvez démarrer plus facilement. Il contient les bibliothèques, les scripts et la configuration de répertoire requis. Ainsi, tout ce que vous avez à faire est de lancer la commande suivante dans votre terminal :
La commande ci-dessus crée un nouveau répertoire appelé foundry-faucet
et initialise un nouveau projet Foundry en utilisant mon modèle. Ce serait la structure du répertoire. Les répertoires et fichiers importants sur lesquels nous voulons nous concentrer sont :
Nous devons également mettre à jour et installer les bibliothèques utilisées ; pour cela lancez les commandes suivantes :
git submodule update --init --recursive
forge install
Maintenant, nous allons implémenter un contrat de robinet pour notre jeton ERC20 qui peut égoutter les jetons sur demande. Nous pouvons également restreindre le nombre de jetons par demande en définissant un limit
qui sera 100
par défaut dans notre contrat.
Ouvrez le src/Faucet.sol
fichier et ajoutez le code suivant :
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
contract Faucet is Ownable {
/// Address of the token that this faucet drips
IERC20 public token;
/// For rate limiting
mapping(address => uint256) public nextRequestAt;
/// Max token limit per request
uint256 public limit = 100;
/// @param _token The address of the faucet's token
constructor(IERC20 _token) {
token = _token;
}
/// Used to send the tokens
/// @param _recipient The address of the tokens recipient
/// @param _amount The amount of tokens required from the faucet
function drip(address _recipient, uint256 _amount) external {
require(_recipient != address(0), "INVALID_RECIPIENT");
require(_amount <= limit, "EXCEEDS_LIMIT");
require(nextRequestAt[_recipient] <= block.timestamp, "TRY_LATER");
nextRequestAt[_recipient] = block.timestamp + (5 minutes);
token.transfer(_recipient, _amount);
}
/// Used to set the max limit per request
/// @dev This method is restricted and should be called only by the owner
/// @param _limit The new limit for the tokens per request
function setLimit(uint256 _limit) external onlyOwner {
limit = _limit;
}
}
Notre contrat de robinet a été ajouté. Nous pouvons maintenant continuer et compiler les contrats en exécutant :
forge build
Si tout se passe bien, vous devriez voir une sortie similaire :
[⠒] Compiling...
[⠒] Compiling 14 files with 0.8.13
Compiler run successful
Sucré! Nous avons mis en place avec succès notre projet Foundry et compilé notre contrat sans aucune erreur ! Bon travail, anon
Maintenant, nous pouvons aller de l'avant et commencer à tester notre contrat Faucet.
Comme vous le savez, contrairement à Hardhat, Forge nous aide à écrire des tests unitaires en utilisant Solidity.
Si vous ouvrez le src/test/Faucet.t.sol
fichier, vous verrez déjà quelques importations d'utilitaires et un contrat BaseSetup.
Il a une configuration initiale qui initialise quelques variables que nous pouvons utiliser dans nos tests. De plus, la setUp()
fonction est similaire à celle d'un beforeEach
casque et elle s'exécute avant chaque test.
La setUp()
fonction crée deux adresses et les étiquette Alice
et Bob
. Il est utile lorsque vous essayez de déboguer via les suivis d'appels car l'étiquette apparaît dans les suivis avec l'adresse.
(Remarque : vm.label est appelé un code de triche et il est spécifique à Forge. Il nous aide à effectuer certaines opérations spéciales en interagissant avec la machine virtuelle dans l'environnement de test. Nous verrons plus de codes de triche au cours de l'article. Pour la liste complète des cheatcodes, vous pouvez vous référer à ce lien )
Remplacez le Faucet.t.sol
par le code suivant pour commencer avec les tests unitaires ;
// SPDX-License-Identifier: MIT
pragma solidity >=0.8;
import {console} from "forge-std/console.sol";
import {stdStorage, StdStorage, Test} from "forge-std/Test.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {Utils} from "./utils/Utils.sol";
import {Faucet} from "../Faucet.sol";
import {MockERC20} from "../MockERC20.sol";
contract BaseSetup is Test {
Utils internal utils;
Faucet internal faucet;
MockERC20 internal token;
address payable[] internal users;
address internal owner;
address internal dev;
uint256 internal faucetBal = 1000;
function setUp() public virtual {
utils = new Utils();
users = utils.createUsers(2);
owner = users[0];
vm.label(owner, "Owner");
dev = users[1];
vm.label(dev, "Developer");
token = new MockERC20();
faucet = new Faucet(IERC20(token));
token.mint(address(faucet), faucetBal);
}
}
Vous pouvez voir que nous avons maintenant créé de nouvelles variables d'état comme faucet
, token
et nous avons également renommé alice
et bob
en owner
et dev
pour une interprétation facile. Dans ce contexte, dev
est quelqu'un qui demande des jetons au robinet alors qu'il owner
est le propriétaire du robinet lui-même.
Dans les trois dernières lignes de la setUp()
méthode, nous déployons un jeton fictif pour le robinet, transmettons son adresse dans le constructeur du new Faucet()
(déploiement du robinet), puis appelons et créons des jetons pour le contrat de robinet déployé.
Maintenant, nous allons hériter du BaseSetup
contrat pour écrire des tests unitaires pour notre contrat Faucet.
Sous le BaseSetup
contrat, ajoutez le code suivant :
contract FaucetTest is BaseSetup {
uint256 amountToDrip = 1;
function setUp() public override {
super.setUp();
}
Comme mentionné précédemment, la setUp()
méthode s'exécute avant tous les cas de test et ici nous appelons la setUp()
méthode du contrat de base qui est le BaseSetup
contrat via super.setUp()
.
Très bien, commençons maintenant à ajouter des tests unitaires pour notre contrat. Juste en dessous de la setUp()
méthode du contrat FaucetTest, ajoutez le morceau de code suivant :
function test_drip_transferToDev() public {
console.log(
"Should transfer tokens to recipient when `drip()` is called"
);
uint256 _inititalDevBal = token.balanceOf(dev);
/// Make sure that initial dev balance is Zero
assertEq(_inititalDevBal, 0);
/// Request some tokens to the dev wallet from the wallet
faucet.drip(dev, amountToDrip);
uint256 _devBalAfterDrip = token.balanceOf(dev);
/// The difference should be equal to the amount requested from the faucet
assertEq(_devBalAfterDrip - _inititalDevBal, amountToDrip);
}
Le code ci-dessus nous aide à tester la drip()
méthode. Le flux de travail est simple.
assertEq(_inititalDevBal, 0);
lignedrip()
méthode à partir de l' faucet
instance de contratdev
après l' drip()
appel dudev
compte avant et après drip()
doit être égale à amountToDrip
, qui est stockée en tant que variable d'état dans le contrat FaucetTestMaintenant, enregistrons le fichier et lançons le test : .forge test
Vous devriez voir la sortie dans votre terminal quelque chose de similaire à ceci :
Cool! Ajoutons quelques tests supplémentaires.
Le test ci-dessus vérifie que la drip()
méthode transfère les jetons au dev
. Donc, nous devons également vérifier que le transfert est valide, ce qui signifie que le solde symbolique du robinet doit être réduit.
Ajoutez le test suivant ci-dessous - la test_drip_transferToDev()
méthode.
function test_drip_reduceFaucetBalance() public {
console.log("The faucet balance should be reduced");
faucet.drip(dev, amountToDrip);
assertEq(token.balanceOf(address(faucet)), faucetBal - amountToDrip);
}
Cela garantit que les jetons que le développeur a reçus sont réellement envoyés depuis le robinet - si c'est le cas, le solde du robinet doit être réduit.
Nous pouvons nous en assurer en exécutant à nouveau la suite de tests :forge test
Si tout se passe bien, votre résultat devrait ressembler à ceci :
Sucré! Si vous avez remarqué, nous avons des console.log
instructions dans nos cas de test, mais elles n'apparaissent pas dans la console. La raison en est que Forge n'affiche pas les journaux par défaut. Pour afficher les journaux, nous devons exécuter la commande avec la verbosité 2 : forge test -vv
affichera les journaux.
De plus, s'il y a des événements qui sont émis par votre contrat, vous pouvez les voir dans les tests avec la verbosité trois (-vvv). Vous pouvez obtenir une trace d'appel détaillée pour vos tests jusqu'au niveau de verbosité cinq, ce qui aide à un meilleur débogage.
Très bien, continuons à ajouter d'autres tests. Nous allons maintenant tester notre mécanisme de limite de débit. Il doit y avoir un intervalle d'au moins cinq minutes avant d'appeler drip()
avec la même adresse de destinataire.
function test_drip_revertIfThrottled() public {
console.log("Should revert if tried to throttle");
faucet.drip(dev, amountToDrip);
vm.expectRevert(abi.encodePacked("TRY_LATER"));
faucet.drip(dev, amountToDrip);
}
vm.expectRevert(bytes32)
est un autre code de triche qui vérifie si le prochain appel revient avec le message d'erreur donné. Dans ce cas, le message d'erreur est TRY_LATER
. Il accepte le message d'erreur sous forme d'octets et non sous forme de chaîne, nous utilisons donc abi.encodePacked
.
Si vous vous souvenez, j'ai mentionné que Forge est livré avec un fuzzer prêt à l'emploi. Essayons.
Nous combinons les tests test_drip_transferToDev
et test_drip_reduceFaucetBalance
, et au lieu de transmettre les entrées manuellement, nous permettons au fuzzer d'entrer les valeurs afin de nous assurer que notre contrat gère différentes entrées.
function test_drip_withFuzzing(address _recipient, uint256 _amount) public {
console.log("Should handle fuzzing");
/// inform the constraints to the fuzzer, so that the tests don't revert on bad inputs.
vm.assume(_amount <= 100);
vm.assume(_recipient != address(0));
uint256 _inititalBal = token.balanceOf(_recipient);
faucet.drip(_recipient, _amount);
uint256 _balAfterDrip = token.balanceOf(_recipient);
assertEq(_balAfterDrip - _inititalBal, _amount);
assertEq(token.balanceOf(address(faucet)), faucetBal - _amount);
}
Le fuzzing est un test basé sur les propriétés. Forge appliquera le fuzzing à tout test prenant au moins un paramètre.
Lorsque vous exécutez la suite de tests, vous pouvez trouver la ligne suivante dans la sortie :
[PASS] test_drip_withFuzzing(address,uint256) (exécutions : 256)
À partir de la sortie ci-dessus, nous pouvons déduire que le fuzzer Forge a appelé la méthode test_drip_withFuzzing() 256
fois avec des entrées aléatoires. Cependant, nous pouvons remplacer ce nombre en utilisant la FOUNDRY_FUZZ_RUNS
variable d'environnement.
Maintenant, ajoutons quelques tests supplémentaires pour la méthode réservée au propriétairesetLimit()
function test_setLimit() public {
console.log("Should set the limit when called by the owner");
faucet.setLimit(1000);
/// the limit should be updated assertEq(faucet.limit(), 1000); } function test_setLimit_revertIfNotOwner() public {console.log("Should revert if not called by Owner"); /// Sets the msg.sender as dev for the next tx vm.prank(dev); vm.expectRevert(abi.encodePacked("Ownable: caller is not the owner")); faucet.setLimit(1000); }
Dans la test_setLimit_revertIfNotOwner()
méthode, un nouveau cheatcode vm.prank(address)
est utilisé. Il blague la machine virtuelle en remplaçant msg.sender par l'adresse donnée ; dans notre cas c'est dev
. Ainsi, le setLimit()
devrait revenir avec le caller is not the owner
message car notre contrat Faucet hérite du Ownable
contrat.
D'accord, assurons-nous qu'aucun test n'échoue en exécutant forge test
à nouveau.
Sweet ! Maintenant, il est temps pour le déploiement.
Créez un nouveau fichier à partir du .env.example
fichier et nommez-le .env
. Veuillez remplir votre INFURA_API_KEY et la PRIVATE_KEY (avec les fonds Kovan testnet).
Une fois que tous les champs sont remplis, vous êtes prêt pour le déploiement sur Kovan. Avant de déployer le robinet, nous devons déployer notre jeton ERC20.
Vous pouvez trouver les scripts de déploiement dans le scripts
répertoire et déployer le jeton MockERC20 sur Kovan testnet en exécutant le ./scripts/deploy_token_kovan.sh
script bash.
La sortie ressemblerait à ceci :
Deployer: (YOUR_DEPLOYMENT_ADDRESS)
Deployed to: 0x1a70d8a2a02c9cf0faa5551304ba770b5496ed80
Transaction hash: 0xa3780d2e3e1d1f9346035144f3c2d62f31918b613a370f416a4fb1a6c2eadc77
Pour vous assurer que la transaction a bien été effectuée, vous pouvez rechercher le hachage de la transaction dans https://kovan.etherscan.io
Copiez l' Deployed to:
adresse, car il s'agit de l'adresse du jeton MockERC20 que nous devons utiliser pour déployer notre contrat Faucet. Pour déployer le robinet, vous pouvez exécuter le ./scripts/deploy_faucet_kovan.sh
script.
Il vous demandera d'entrer l'adresse du jeton ; puis entrez l'adresse de jeton MockERC20 copiée qui a été déployée précédemment.
La sortie devrait ressembler à ceci :
Woohoo ! Nous avons compilé, testé et déployé avec succès notre contrat sur le testnet de Kovan à l'aide de Forge
Nous devons encore vérifier le contrat sur Etherscan et également créer des jetons MockERC20 sur le robinet (vous pouvez utiliser cast pour cela !) pour que cela fonctionne comme prévu. Je vous laisse cela comme un exercice pour l'essayer vous-mêmes !
Comme toujours, vous pouvez trouver le référentiel GitHub pour cet article ici .
Dans cet article, nous n'avons couvert que quelques morceaux de Forge. Foundry est un cadre très puissant pour les contrats intelligents et il se développe également rapidement.
Il existe d'autres fonctionnalités intéressantes telles que la couverture de code, la vérification des contrats, les instantanés de gaz, les suivis d'appels et le débogage interactif. N'hésitez pas à jouer avec le dépôt en testant plus de fonctionnalités. Bon codage!
Source : https://blog.logrocket.com/unit-testing-smart-contracts-forge/