本文介绍了hardhat-tracer工具,它可以帮助开发者在测试、学习智能合约协议或分支时追踪智能合约事务。文章通过一个简单的ERC20合约示例,展示了如何使用hardhat-tracer的trace flag来追踪函数调用,并展示了如何添加地址名称标签以提高可读性。hardhat-tracer在处理复杂的代码(如大型项目分支)时非常有用,可以帮助理解代码层面上发生的事情。
你是否曾注意到在测试或学习智能合约协议或 fork 时需要跟踪智能合约交易的需求?
hardhat-tracer 可以帮助你。
在这里找到这个工具:https://github.com/zemse/hardhat-tracer
从 Readme 文件中,可以很清楚地了解如何使用该工具。但是,我仍然想通过一些简单的合约示例来演示该工具的用法。
在本示例中,我们将看到 trace 标志,但它也提供其他标志,如 fulltrace 和 hash 用于跟踪。
以下是我们使用的合约:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol";
contract ERC20Mock is ERC20, Ownable { constructor( string memory name, string memory symbol, uint256 supply ) public ERC20(name, symbol) { _mint(msg.sender, supply); } }
2\. OtherContract(一个调用 EventEmitter 合约来 emit 事件的合约):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./EventEmitter.sol";
contract OtherContract{
address emitter;
function takeTokenAndCallOtherFunction(address _tokenAddr) public {
IERC20(_tokenAddr).transferFrom(msg.sender, address(this), 5 ether);
emitTokensTaken(msg.sender);
}
function setEmitterAddress(address _emitter) external {
emitter=_emitter;
}
function emitTokensTaken(address _from) private {
EventEmitter(emitter).emitTokenFrom(_from);
}
}
EventEmitter (实际 emit 事件的合约)
pragma solidity 0.8.4;
contract EventEmitter{
event TokensFrom(address From);
function emitTokenFrom(address _from) public {
emit TokensFrom(_from);
}
}
一个快速的工作流程:
调用者 approve OtherContract 花费 5 个代币。
调用者使用 OtherContract 合约中的 setEmitterAddress 函数设置 EventEmitter 合约地址。
调用者调用 OtherContract 合约的 takeTokenAndCallOtherFunction 函数,该函数然后调用 emitTokensTaken 函数。
emitTokensTaken 函数调用位于 EventEmitter 合约中的 emitTokenFrom 函数。
emitTokenFrom 函数只是 emit TokensFrom 事件。
使用 npm i hardhat-tracer 安装
将其添加到 hardhat.config.js:require(“hardhat-tracer”);
在下面的测试中,你可以看到四个函数调用:
it("test",async () => {
// Approve OtherContract.
console.log("------------------------------------ approve tokens ------------------------------------");
await token1Instance.approve(OtherContractInstance.address,ethers.utils.parseEther("5"));
console.log("----------------------------------------------------------------------------------------\n");
console.log("----------------------------------- set emitter addr -----------------------------------");
await OtherContractInstance.setEmitterAddress(EventEmitterInstance.address);
console.log("----------------------------------------------------------------------------------------\n");
console.log("-------------------------- call takeTokenAndCallOtherFunction --------------------------")
await OtherContractInstance.takeTokenAndCallOtherFunction(token1Instance.address);
console.log("----------------------------------------------------------------------------------------\n")
console.log("---------------------- failed takeTokenAndCallOtherFunction call -----------------------")
await expect(OtherContractInstance.takeTokenAndCallOtherFunction(token1Instance.address)).to.be.revertedWith("ERC20: insufficient allowance");
console.log("----------------------------------------------------------------------------------------\n")
});
让我们用 hardhat-tracer 跟踪这些调用:
现在只需使用 test 命令 + trace 标志运行测试文件:例如:> npx hardhat test test/token-trace.js --trace
你可以在终端中看到交易跟踪:
Test screenshot 1
hardhat-tracer 允许你设置地址名称标签以识别地址。你可以在测试调用之前或创建合约实例之后添加它们来设置地址,如下所示:
hre.tracer.nameTags[OtherContractInstance.address] = "OtherContract";
hre.tracer.nameTags[EventEmitterInstance.address] = "EventEmitter";
hre.tracer.nameTags[token1Instance.address] = "TKN1";
hre.tracer.nameTags[alice.address] = "Alice";
添加这些标签后,你可以在跟踪中看到地址的直接名称/标签:
traces when address tags added
在上面的屏幕截图中,你可以看到使用构造函数参数创建合约的跟踪,以及所有权从零地址转移到 msg.sender。
回到第一个测试用例调用,即 approve 代币的调用:

第二次调用是 OtherContract.setEmitterAddress() 来设置 emitter 地址。

第三次是一个有趣的调用,这个调用基本上调用了另一个函数,然后再次调用 Emitter 合约中的函数。下面的屏幕截图显示了 OtherContract.takeTokenAndCallOtherFunction() 函数的跟踪,该函数调用 transferFrom() 函数,该函数然后将 approve 设置为零(approve 为 5 个代币)并将 5 个代币转账到 OtherContract 地址,你可以看到 Approval 和 Transfer 的事件跟踪。
之后,它调用 emitTokensTaken() 函数,然后调用 emitter 合约中的 emitTokenFrom() 函数,该函数最终 emit TokensFrom 事件。你可以在下面的屏幕截图中看到这一点:

最后,最后一个是失败的 takeTokenAndCallOtherFunction() 调用,它将 revert。在这里,你可以看到 takeTokenAndCallOtherFunction() 正在调用 transferFrom(),由于 0 allowance(takeTokenAndCallOtherFunction 函数至少需要 5 个代币的 allowance)而失败。然后,你可以看到 allowance 不足的 revert 消息。

上面的例子非常小,并且没有复杂的外部合约调用,但是在浏览像大型项目 fork 这样的复杂代码,甚至是包含许多调用的 fork 网络时,跟踪对于理解代码级别上发生的事情非常有用。
- 原文链接: calibersec.com/tracing-s...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!