在 Solidity 开发中,event
是智能合约与链下系统交互的重要桥梁。在单元测试中验证事件的触发和参数正确性,是保证合约逻辑正确的关键环节。本文将结合 Foundry,全面讲解事件的测试方法,包括严格顺序匹配、顺序忽略,以及解码非 indexed
参数。
假设我们有一个简单的 Token 合约,transfer
函数会触发一个 Transfer
事件:
contract Token {
event Transfer(address indexed from, address indexed to, uint256 amount);
function transfer(address to, uint256 amount) external {
emit Transfer(msg.sender, to, amount);
}
function batchTransfer(address[] calldata recipients, uint256 amount) external {
for (uint i = 0; i < recipients.length; i++) {
emit Transfer(msg.sender, recipients[i], amount);
}
}
}
在 Foundry 中,可以使用 vm.expectEmit
声明预期事件,再调用触发事件的函数:
function testEmitTransfer() public {
address to = address(0xBEEF);
uint256 amount = 100;
// 告诉 Foundry:我期望捕捉一个 Transfer 事件
vm.expectEmit(true, true, false, true);
// 写出期望事件
emit Token.Transfer(address(this), to, amount);
// 调用触发事件的函数
token.transfer(to, amount);
}
vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData)
checkTopic1~3
:是否检查事件中 indexed
参数checkData
:是否检查非 indexed
数据通过这种方式,你可以灵活忽略不需要关注的参数。
对于批量操作,可能会触发多个事件。以 BatchToken
为例:
contract BatchToken {
event Transfer(address indexed from, address indexed to, uint256 amount);
function batchTransfer(address[] calldata recipients, uint256 amount) external {
for (uint i = 0; i < recipients.length; i++) {
emit Transfer(msg.sender, recipients[i], amount);
}
}
}
function testBatchTransferEmitsMultipleEvents() public {
address ;
recipients[0] = address(0xA11CE);
recipients[1] = address(0xB0B);
recipients[2] = address(0xCAro1);
uint256 amount = 100;
// 每个事件都需要单独 expectEmit
vm.expectEmit(true, true, false, true);
emit BatchToken.Transfer(address(this), recipients[0], amount);
vm.expectEmit(true, true, false, true);
emit BatchToken.Transfer(address(this), recipients[1], amount);
vm.expectEmit(true, true, false, true);
emit BatchToken.Transfer(address(this), recipients[2], amount);
token.batchTransfer(recipients, amount);
}
⚠️ 注意:Foundry 严格按照事件顺序匹配,如果顺序不一致,测试会失败。
vm.expectEmit(true, true, false, true);
emit BatchToken.Transfer(address(this), recipients[1], amount); // 顺序错误
vm.expectEmit(true, true, false, true);
emit BatchToken.Transfer(address(this), recipients[0], amount);
token.batchTransfer(recipients, amount);
运行时会报错:
...
[FAIL: log != expected log] testBatchTransferWrongOrder() (gas: 25687)
Traces:
[25687] TokenTest::testBatchTransferWrongOrder()
├─ [0] VM::expectEmit(true, true, false, true)
│ └─ ← [Return]
├─ emit Transfer(from: TokenTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000B0b, amount: 100)
├─ [0] VM::expectEmit(true, true, false, true)
│ └─ ← [Return]
├─ emit Transfer(from: TokenTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x00000000000000000000000000000000000A11cE, amount: 100)
├─ [0] VM::expectEmit(true, true, false, true)
│ └─ ← [Return]
├─ emit Transfer(from: TokenTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000C0FFEE, amount: 100)
├─ [7727] Token::batchTransfer([0x00000000000000000000000000000000000A11cE, 0x0000000000000000000000000000000000000B0b, 0x0000000000000000000000000000000000C0FFEE], 100)
│ ├─ emit Transfer(from: TokenTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x00000000000000000000000000000000000A11cE, amount: 100)
│ ├─ emit Transfer(from: TokenTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000B0b, amount: 100)
│ ├─ emit Transfer(from: TokenTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000C0FFEE, amount: 100)
│ └─ ← [Stop]
└─ ← [Revert] log != expected log
...
如果不关心顺序,可以使用 vm.recordLogs()
+ vm.getRecordedLogs()
手动检查事件:
vm.recordLogs();
token.batchTransfer(recipients, amount);
Vm.Log[] memory entries = vm.getRecordedLogs();
// 遍历日志,验证事件存在
bool foundAlice;
for (uint i = 0; i < entries.length; i++) {
bytes32 expectedSig = keccak256("Transfer(address,address,uint256)");
assertEq(entries[i].topics[0], expectedSig);
address to = address(uint160(uint256(entries[i].topics[2])));
if (to == recipients[0]) foundAlice = true;
}
assertTrue(foundAlice, "Alice not found");
这种方式不依赖顺序,非常适合批量事件或异步事件验证。
事件的非 indexed
参数会存放在 data
字段,需要用 abi.decode
解码:
for (uint i = 0; i < entries.length; i++) {
// 解码 indexed 参数
address from = address(uint160(uint256(entries[i].topics[1])));
address to = address(uint160(uint256(entries[i].topics[2])));
// 解码 data
uint256 decodedAmount = abi.decode(entries[i].data, (uint256));
assertEq(decodedAmount, amount);
}
这样就能同时验证 indexed 和 非 indexed 参数。
完整的测试文件内容如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import {Token} from "../src/Token.sol";
contract TokenTest is Test {
Token token;
function setUp() public {
token = new Token();
}
event Transfer(address indexed from, address indexed to, uint256 amount);
function testEmitTransfer() public {
address to = address(0xBEEF);
uint256 amount = 100;
// 告诉 Foundry:我期望捕捉一个 Transfer 事件
vm.expectEmit(true, true, false, true);
// 写出期望事件
emit Token.Transfer(address(this), to, amount);
// 调用触发事件的函数
token.transfer(to, amount);
}
function testBatchTransferEmitsMultipleEvents() public {
address[] memory recipients = new address[](3);
recipients[0] = address(0xA11CE);
recipients[1] = address(0xB0B);
recipients[2] = address(0xC0FFEE);
uint256 amount = 100;
// 每个事件都需要单独 expectEmit
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), recipients[0], amount);
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), recipients[1], amount);
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), recipients[2], amount);
token.batchTransfer(recipients, amount);
}
function testBatchTransferWrongOrder() public {
address[] memory recipients = new address[](3);
recipients[0] = address(0xA11CE);
recipients[1] = address(0xB0B);
recipients[2] = address(0xC0FFEE);
uint256 amount = 100;
// 每个事件都需要单独 expectEmit
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), recipients[1], amount);
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), recipients[0], amount);
vm.expectEmit(true, true, false, true);
emit Transfer(address(this), recipients[2], amount);
token.batchTransfer(recipients, amount);
}
function testBatchTransferIgnoreOrder() public {
address[] memory recipients = new address[](3);
recipients[0] = address(0xA11CE);
recipients[1] = address(0xB0B);
recipients[2] = address(0xC0FFEE);
uint256 amount = 100;
// 开始记录日志
vm.recordLogs();
// 执行函数
token.batchTransfer(recipients, amount);
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries.length, 3, "Should emit 3 events");
// keccak256("Transfer(address,address,uint256)")
bytes32 expectedSig = keccak256("Transfer(address,address,uint256)");
bool foundAlice;
for (uint i = 0; i < entries.length; i++) {
assertEq(entries[i].topics[0], expectedSig);
// indexed 参数
address from = address(uint160(uint256(entries[i].topics[1])));
address to = address(uint160(uint256(entries[i].topics[2])));
// 非 indexed 参数
uint256 decodedAmount = abi.decode(entries[i].data, (uint256));
emit log_named_address("to", to);
emit log_named_uint("decodedAmount", decodedAmount);
assertEq(from, address(this), "Wrong sender");
assertEq(decodedAmount, amount, "Wrong amount");
assertEq(entries[i].topics[0], expectedSig);
if (to == recipients[0]) foundAlice = true;
}
assertTrue(foundAlice, "Alice not found");
}
}
vm.expectEmit
适合严格顺序匹配的事件测试。vm.recordLogs
+ getRecordedLogs
适合忽略顺序或复杂验证。abi.decode
解码。通过掌握这些技巧,我们就可以在 Foundry 中高效、精准地验证 Solidity 合约事件逻辑,保证合约行为与预期一致。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。