
More about Foundry tools
本文最后更新于 2024-05-25,本文发布时间距今超过 90 天, 文章内容可能已经过时。最新内容请以官方内容为准
More about Foundry tools
Cast
使用 Cast 工具发送交易
使用 Cast 工具发送交易,并与已部署在以太坊链上的智能合约进行交互。
掌握这些技能对于智能合约开发者来说至关重要,它允许你执行合约函数、触发合约的 fallback 和 receive 函数。
调用合约函数
如果你拥有账户的私钥和合约地址,你可以通过 cast send 命令调用合约上的任何函数。
这为开发者提供了一种直接与智能合约互动的方式,无论是存款、提款还是修改合约状态。
使用方法示例:
cast send --private-key <private_key_addr> <contract_addr> "exampleFunc(uint256)" <argument_value_of_the_function>
示例:存款函数
例如,要向合约发送一个存款请求,你可以使用以下命令:
cast send --private-key 0x123... 0xabc... "deposit(uint256)" 10
这条命令会向地址为 0xabc... 的合约发送一个调用 deposit 函数的请求,存款金额为 10 wei。
触发 Fallback 函数
如果调用合约中不存在的函数,将自动触发 Fallback 函数。这可以用于测试合约的异常处理或特定的功能。
使用方法示例:
cast send --private-key <private_key_addr> <contract_addr> "dummy()"
示例:调用不存在的函数
你可以通过以下命令测试合约的 Fallback 函数响应:
cast send --private-key 0x123... 0xabc... "dummy()"
触发 Receive 函数
通过向合约发送以太币(Ether),可以触发合约的 Receive 函数。这对于接受捐款或处理支付非常有用。
使用方法示例:
cast send --private-key <private_key_addr> <contract_addr> --value 10gwei
示例:发送以太币
以下命令展示了如何发送 10 gwei 的以太币到合约,触发接收函数:
cast send --private-key 0x123... 0xabc... --value 10gwei
使用 Cast 工具从 Etherscan 获取智能合约的源代码
使用 Cast 工具从 Etherscan 获取智能合约的源代码。
对于区块链开发者来说,能够审查和理解链上合约的源代码是非常重要的,这有助于分析合约功能和验证其安全性。
获取合约源代码
cast etherscan-source 命令允许你快速从 Etherscan 网站获取指定合约的源代码。
这是一个强大的功能,因为它让开发者能够审查在以太坊上公开部署的任何合约的具体实现。
使用方法示例:
cast etherscan-source <contract_address>
示例:获取源代码
如果你想查看特定合约的源代码,可以通过以下命令实现:
cast etherscan-source 0x123...
这条命令将查询 Etherscan 以获取地址为 0x123... 的智能合约的源代码,并将其显示出来。
Forge
Forge Test
分叉模式 (fork)
分叉测试(Fork Testing)是区块链开发中一项重要的测试方法,尤其对于基于以太坊这类可编程区块链的智能合约开发来说尤为重要。
使用 Forge 工具进行分叉测试的两种不同方法:
- 分叉模式(Forking Mode)
- 分叉作弊码(Forking Cheatcodes)。
分叉模式
分叉模式允许开发者通过指定的 RPC URL 在一个分叉的环境中运行所有测试。
使用分叉模式时,可以通过--fork-url 标志传递一个 RPC URL,如下所示:
forge test --fork-url <your_rpc_url>
在分叉模式下,以下值将反映在分叉时刻链的状态:
- block_number 区块号
- chain_id 链 ID
- gas_limit 燃料上限
- gas_price 燃料价格
- block_base_fee_per_gas 每单位燃料的基础费用
- block_timestamp 区块时间戳
如果需要从特定区块开始分叉,可以使用--fork-block-number 标志:
forge test --fork-url <your_rpc_url> --fork-block-number 1
-
缓存
如果同时指定了--fork-url 和--fork-block-number,那么该区块的数据将被缓存以供未来的测试运行使用。数据缓存位置在
~/.foundry/cache/rpc/<chain name>/<block number>
。清除缓存可以通过删除该目录或运行 forge clean(这将移除所有构建产物和缓存目录)。
-
改进的追踪
Forge 支持使用 Etherscan 识别分叉环境中的合约。
通过--etherscan-api-key 标志传递 Etherscan API 密钥:
forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key>
分叉作弊码
分叉作弊码提供了一种在 Solidity 测试代码中以编程方式进入分叉模式的方法。
这种技术使得开发者能够在基于测试的基础上使用分叉模式,并在测试中处理多个分叉。本课将详细介绍分叉作弊码的使用方法、实例代码以及存储的分隔和持久性特征。
-
分叉作弊码的使用
与通过 forge CLI 参数配置分叉模式不同,分叉作弊码允许我们在 Solidity 测试代码中直接创建、选择和管理多个分叉。
每个分叉通过一个唯一的 uint256 标识符来识别。
-
测试函数的隔离性
重要的是要记住,所有测试函数都是隔离的,这意味着每个测试函数都在 setUp 后的状态副本中执行,并在其自己的独立 EVM 中执行。
因此,在 setUp 期间创建的分叉在测试中可用。
-
创建和选择分叉的实例
以下示例展示了如何使用分叉作弊码在测试合约中创建和选择分叉。
3.1 定义分叉标识符contract ForkTest is Test { uint256 mainnetFork; uint256 optimismFork; }
在 ForkTest 合约中,我们首先定义两个变量 mainnetFork 和 optimismFork 来存储主网和 Optimism 网络分叉的标识符。
-
创建分叉
function setUp() public { mainnetFork = vm.createFork(MAINNET_RPC_URL); optimismFork = vm.createFork(OPTIMISM_RPC_URL); }
在 setUp 函数中,使用 vm.createFork 方法创建两个不同的分叉,并将它们的标识符分别赋值给 mainnetFork 和 optimismFork。
-
选择分叉
function testCanSelectFork() public { vm.selectFork(mainnetFork); assertEq(vm.activeFork(), mainnetFork); }
通过 vm.selectFork 方法,我们选择并激活一个特定的分叉,然后通过 vm.activeFork() 验证当前激活的分叉是否正确。
-
分叉的独立和持久性
每个分叉都是一个独立的 EVM,使用完全独立的存储。msg.sender 和测试合约本身的状态在分叉间是持久的。换句话说,当分叉 A 处于活动状态时所做的所有更改仅记录在分叉 A 的存储中,并且在选择另一个分叉时不可用。
-
创建新合约示例
function testCreateContract() public { vm.selectFork(mainnetFork); SimpleStorageContract simple = new SimpleStorageContract(); simple.set(100); assertEq(simple.value(), 100); vm.selectFork(optimismFork); // 这里尝试访问 simple.value() 将会失败,因为 simple 仅存在于 mainnetFork 中 }
此示例展示了在活动分叉上创建新合约的过程。当从一个分叉切换到另一个分叉时,只有那些被标记为持久的账户和合约才能跨分叉访问。
-
持久合约的创建
在测试智能合约时,尤其是在需要跨不同区块链分叉环境测试时,持久性账户(或合约)的概念变得非常重要。持久性账户允许我们在一个分叉环境中创建和修改状态,并在另一个分叉环境中保持这些状态不变。下面的代码示例展示了如何创建一个持久性合约并在不同的分叉环境中使用它。
contract SimpleStorageContract { uint256 public value; function set(uint256 _value) public { value = _value; } } // 创建并测试持久性合约 function testCreatePersistentContract() public { // 首先,选择一个分叉环境 vm.selectFork(mainnetFork); // 在该分叉环境中部署并初始化合约 SimpleStorageContract simple = new SimpleStorageContract(); simple.set(100); // 确认合约的状态设置正确 assertEq(simple.value(), 100); // 接下来,将合约标记为持久性 vm.makePersistent(address(simple)); // 验证合约已被正确标记为持久性 assert(vm.isPersistent(address(simple))); // 然后,切换到另一个分叉环境 vm.selectFork(optimismFork); // 验证即使在新的分叉环境中,合约仍被标记为持久性 assert(vm.isPersistent(address(simple))); // 最后,确认持久性合约的状态在新的分叉环境中保持不变 assertEq(simple.value(), 100); }
- 通过 vm.makePersistent(address) 方法,我们将 simple 合约标记为持久性,确保其状态(在这个例子中是通过 set 方法设置的值)在不同分叉环境中保持不变。
- 接着,通过 vm.isPersistent(address) 验证该合约是否被正确标记为持久性。当我们切换到另一个分叉环境(如 Optimism 分叉)时,通过再次调用 vm.isPersistent(address) 和 assertEq(simple.value(), 100) 来确认合约仍然保持其持久性状态以及其存储的值。
这种能力特别重要,因为它允许开发者在多个分叉环境中测试合约的行为和交互,而不需要重新部署或重新初始化合约状态,从而提高了测试的效率和效果。
差异模糊测试
差异模糊测试是差异测试的一个扩展,它通过编程方式生成大量输入值来发现可能被常规测试遗漏的错误和边缘情况。
这种测试方法在处理复杂算法或需要处理大量输入情况的软件中尤为重要。
差异模糊测试的基本概念
差异模糊测试自动化地生成输入数据,并将这些数据用于多个实现的测试中。
这样,不仅可以检测实现是否符合预期,还可以揭示那些只在特定数据组合或极端条件下出现的问题。
-
如何使用差异模糊测试
在使用差异模糊测试时,你通常会遇到以下几个步骤:
1.定义输入空间:确定哪些输入数据是测试的焦点。
2.生成输入数据:使用模糊测试工具生成广泛的随机数据。
3.执行测试:将生成的数据同时应用于不同的实现。
4.比较输出:分析各实现的输出,寻找不一致之处。 -
使用 Foundry 进行差异模糊测试
Foundry 提供了一套工具,使得在智能合约开发中实施差异模糊测试变得简单。
通过使用 Foundry 的 ffi 功能,可以调用外部命令,从而集成更广泛的测试环境和框架。
示例:使用 ffi 进行差异模糊测试
假设我们要测试一个函数的两个实现是否一致,以下是一个简单的测试合约示例:
import "forge-std/Test.sol"; contract DifferentialFuzzTest is Test { function testInputDifferentialFuzz(uint256 input) public { uint256 result1 = implementation1(input); uint256 result2 = implementation2(input); assertEq(result1, result2, "The outputs of the two implementations do not match"); } // 伪代码实现 function implementation1(uint256 input) internal pure returns (uint256) { return input * 2; } function implementation2(uint256 input) internal pure returns (uint256) { return input << 1; } }
在此示例中,
implementation1
和implementation2
是两种不同的实现,该测试会验证它们在相同输入下是否产生相同的输出。
实战应用:默克尔树实现的差异测试
使用差异测试方法来验证默克尔树实现的正确性。
默克尔树是区块链技术中广泛使用的一种数据结构,用于有效地验证数据块的完整性。
我们将通过一个具体的案例学习如何在实际中应用差异测试和模糊测试来确保不同实现之间的一致性。
-
默克尔树的基本概念
默克尔树是一种二叉树,其中每个非叶子节点是其子节点加密哈希的结果。
这种结构允许高效且安全地验证数据内容,是区块链技术中的核心组成部分。
-
差异测试的应用场景
在多个实现之间进行差异测试可以确保所有实现都符合预期的行为规范。
这对于区块链开发尤为重要,因为数据的完整性和安全性是最高优先级。
示例:默克尔树实现的差异测试
在这个示例中,我们将比较两种不同语言(Solidity 和 JavaScript)编写的默克尔树根生成算法的输出。
使用 Foundry 和 JavaScript 进行差异测试
我们的目标是验证用 Solidity 和 JavaScript 编写的默克尔树根生成函数是否产生相同的输出。
我们将使用 Foundry 的 ffi 功能来调用外部 JavaScript 实现,并将结果与 Solidity 实现进行比较。
-
Solidity 实现
我们首先创建一个简单的 Solidity 实现,它生成默克尔树的根:// Solidity 版本的默克尔树实现 function generateMerkleRoot(bytes32[] memory leaves) public pure returns (bytes32) { // 示例代码:具体实现细节省略 return keccak256(abi.encodePacked(leaves)); }
-
JavaScript 实现和 Foundry 的 ffi 调用
然后,我们使用 Foundry 的 ffi 功能来调用一个外部的 JavaScript 脚本,该脚本也生成默克尔树的根:
function testMerkleRootMatchesJSImplementation(bytes32[] memory leaves) public { string[] memory args = new string[](3); args[0] = "node"; args[1] = "./calculateMerkleRoot.js"; args[2] = leaves.toHexString(); // 假设已实现转换为 hex 字符串的功能 bytes memory jsResult = vm.ffi(args); bytes32 jsMerkleRoot = abi.decode(jsResult, (bytes32)); bytes32 solMerkleRoot = generateMerkleRoot(leaves); assertEq(solMerkleRoot, jsMerkleRoot, "Merkle roots do not match"); }
-
差异测试高级应用
探讨差异测试的高级应用,特别是针对已知边缘情况和复杂场景的差异测试。
这种测试方法对于确保软件的健壮性和可靠性至关重要,尤其是在处理复杂的数据结构和算法时。
-
高级差异测试的重要性
高级差异测试可以帮助开发者发现那些在常规测试中难以捕捉到的错误。
它通过系统地探索输入空间的边缘情况,确保软件在各种情况下都能正常工作。
示例:针对 OpenZeppelin 的 Merkle 证明库的差异模糊测试
在这个示例中,我们将展示如何使用差异模糊测试对 Solidity 的 Merkle 证明库进行测试,以确保它与另一个已的实现 相兼容。
-
测试设置
我们将使用 Foundry 工具和 OpenZeppelin 的合约库进行测试。
假设我们有两个不同的 Merkle 证明实现:
- 一个是我们自己的实现,
- 另一个是 OpenZeppelin 的实现。
import "forge-std/Test.sol"; import "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; contract MerkleProofDifferentialTest is Test { Merkle myMerkle; bytes32[] public testLeaves; bytes32 public root; function setUp() public { testLeaves = [keccak256("Leaf1"), keccak256("Leaf2"), keccak256("Leaf3")]; root = myMerkle.computeRoot(testLeaves); } function testMerkleProofCompatibility(bytes32[] memory _leaves, uint256 node) public { vm.assume(_leaves.length > 1); vm.assume(node < _leaves.length); bytes32[] memory proof = myMerkle.getProof(_leaves, node); bytes32 valueToProve = _leaves[node]; bool myVerified = myMerkle.verifyProof(root, proof, valueToProve); bool ozVerified = MerkleProof.verify(proof, root, valueToProve); assertTrue(myVerified == ozVerified, "Proof verification results differ between implementations"); } }
-
测试执行
通过这种测试,我们可以验证不同实现在相同的数据输入下是否产生一致的验证结果。
这种方法尤其适用于加密算法和数据验证逻辑,确保不同库之间的兼容性和一致性。
-
针对已知边缘情况的差异测试
差异测试不仅限于自动生成的随机数据。它也适用于已知的边缘情况,这些情况可能导致算法表现异常。
示例:测试边缘情况
function testKnownEdgeCases() public { // 假设已经识别了一些引起问题的特定数据点 bytes32[] memory problematicLeaves = [keccak256("EdgeCase1"), keccak256("EdgeCase2")]; root = myMerkle.computeRoot(problematicLeaves); for (uint i = 0; i < problematicLeaves.length; i++) { bytes32[] memory proof = myMerkle.getProof(problematicLeaves, i); bytes32 valueToProve = problematicLeaves[i]; bool verified = myMerkle.verifyProof(root, proof, valueToProve); assertTrue(verified, "Failed to verify known edge case"); } }