本文介绍了如何将 Tenderly 集成到 Hardhat 项目中,以调试智能合约,并提供了一个使用 Tenderly 进行调试的示例,包括 Staking 和 Rewards 合约的部署和交互。文章还分享了在使用 Tenderly 时可能遇到的问题和解决方法,例如如何正确访问合约对象以调用函数,以及如何配置 Hardhat 以使用 Tenderly 进行合约验证。
请继续关注关于区块链桥接中常见漏洞、我的审计思维模型等内容的资源 ✨🔒
在本文中,我们将探讨如何将 Tenderly 与一个实现了 staking(质押)和奖励系统的示例 Hardhat 项目集成。
虽然 Tenderly 的文档会帮助你完成整个过程,但我相信我可以给你更多关于哪里可能出错的想法。
从安全角度来看,在审计协议时,你会遇到多种情况,其中跟踪控制流中各种变量值是不可行的。例如,可能会有多个嵌套调用,并且在每个调用帧中更新值。在许多大型项目中,凭空跟踪所有内容是不切实际的,并且如果你使用的是 Hardhat 项目,则也无法进行直接调试。
虽然我们已经探索了一些 本地选项。这将是一个更加用户友好的选择。可以通过共享 TestNet 进度来和多个团队协作。
该项目由三个主要的简化智能合约组成:
对于提到的合约,合约功能 / 入口点的简短说明:
pragma solidity ^0.8.0;
import "./Token.sol";
import "./Rewards.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @notice This code has not been audited and may contain vulnerabilities.
* Never use in the production.
*/
contract Staking is ReentrancyGuard {
MyToken public token;
Rewards public rewards;
mapping(address => uint256) public stakedAmounts;
uint256 public totalStaked;
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
constructor(MyToken _token, Rewards _rewards) {
token = _token;
rewards = _rewards;
}
function stake(uint256 amount) public nonReentrant {
require(amount > 0, "Amount must be greater than 0");
require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed");
stakedAmounts[msg.sender] += amount;
totalStaked += amount;
emit Staked(msg.sender, amount);
}
function unstake(uint256 amount) public nonReentrant {
require(stakedAmounts[msg.sender] >= amount, "Insufficient staked amount");
stakedAmounts[msg.sender] -= amount;
totalStaked -= amount;
require(token.transfer(msg.sender, amount), "Transfer failed");
rewards.claimRewards(msg.sender);
emit Unstaked(msg.sender, amount);
}
function getStakedAmount(address staker) public view returns (uint256) {
return stakedAmounts[staker];
}
function getTotalStaked() public view returns (uint256) {
return totalStaked;
}
}
Staking.sol
pragma solidity ^0.8.0;
import "./Staking.sol";
/**
* @notice This code has not been audited and may contain vulnerabilities.
* Never use in the production.
*/
contract Rewards {
Staking public staking;
mapping(address => uint256) public rewards;
constructor() {}
function initialize(Staking _staking) external {
require(address(staking) == address(0), "Already initialized");
staking = _staking;
}
function calculateRewards(address staker) public view returns (uint256) {
uint256 stakedAmount = staking.getStakedAmount(staker);
// Simplified reward calculation
return stakedAmount / 10;
}
function claimRewards(address staker) external {
require(msg.sender == address(staking), "Only staking contract can call this function");
uint256 reward = calculateRewards(staker);
rewards[staker] += reward;
}
}
Rewards.sol
Staking.sol
)MyToken
和 Rewards
合约地址初始化 staking 合约。MyToken
代币,更新其质押金额和总质押金额。MyToken
代币,更新其质押金额、总质押金额和领取奖励。Rewards.sol
)从现在开始,你可以按照官方文档,但我还有一些你可能在文档中找不到的额外内容想要分享。
brew tap tenderly/tenderly
brew install tenderly
tenderly login
登录后,你可以检查 tenderly whoami
以确认。
npm install --save-dev @tenderly/hardhat-tenderly
const tenderly = require("@tenderly/hardhat-tenderly");
。导入方式会因 .ts 或 .js 文件而异。tdly.setup()
并将 automaticVerifications
选项设置为 true,如下所示:tenderly.setup({ automaticVerifications: true });
但如果你使用的版本高于此版本,则可以将 TENDERLY_AUTOMATIC_VERIFICATION
环境变量设置为 true
,例如使用 .env
文件。否则,你可能会收到如下警告。你需要在 Tenderly 仪表板中使用 Tenderly 创建一个虚拟 TestNet,并添加一个包含 Tenderly 虚拟 TestNet URL 和 Tenderly 配置对象的网络:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as tenderly from "@tenderly/hardhat-tenderly";
// tenderly.setup({ automaticVerifications: true }); // <- 取决于你使用的版本,如上所述。
const config: HardhatUserConfig = {
solidity: "0.8.19",
networks: {
virtualMainnet: {
url: process.env.TENDERLY_VIRTUAL_MAINNET_RPC!,
},
},
tenderly: {
// https://docs.tenderly.co/account/projects/account-project-slug
project: "YOUR PROJECT",
username: "YOUR USERNAME",
},
};
export default config;
编写部署脚本。脚本的工作方式未进行解释,因为假设读者已经知道它了。
const { ethers } = require("hardhat");
const { expect } = require("chai");
async function main() {
const [owner, addr1, addr2] = await ethers.getSigners();
// Deploy the token contract
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy(ethers.parseEther("1000000"));
await token.waitForDeployment();
console.log("Token deployed to:", token.target);
// Deploy the rewards contract
const Rewards = await ethers.getContractFactory("Rewards");
const rewards = await Rewards.deploy();
await rewards.waitForDeployment();
console.log("see rewards instance obj", rewards);
console.log("Rewards deployed to:", rewards.target);
// Deploy the staking contract
const Staking = await ethers.getContractFactory("Staking");
const staking = await Staking.deploy(token.target, rewards.target);
await staking.waitForDeployment();
console.log("Staking deployed to:", staking.target);
// Initialize the rewards contract with the staking contract address
await rewards.nativeContract.initialize(staking.target);
// Mint some tokens for testing
await token.nativeContract.transfer(await addr1.getAddress(), ethers.parseEther("1000"));
await token.nativeContract.transfer(await addr2.getAddress(), ethers.parseEther("1000"));
// Check and log the balances
const balanceAddr1 = await token.nativeContract.balanceOf(await addr1.getAddress());
const balanceAddr2 = await token.nativeContract.balanceOf(await addr2.getAddress());
console.log(`Balance of addr1: ${balanceAddr1} tokens`);
console.log(`Balance of addr2: ${balanceAddr2} tokens`);
// Test cases
console.log("Running test cases...");
// Test staking tokens
await token.nativeContract.connect(addr1).approve(staking.nativeContract.getAddress(), ethers.parseEther("100"));
await staking.nativeContract.connect(addr1).stake(ethers.parseEther("100"));
expect(await staking.nativeContract.getStakedAmount(await addr1.getAddress())).to.equal(ethers.parseEther("100"));
expect(await staking.nativeContract.getTotalStaked()).to.equal(ethers.parseEther("100"));
// Test unstaking tokens
await staking.nativeContract.connect(addr1).unstake(ethers.parseEther("50"));
expect(await staking.nativeContract.getStakedAmount(await addr1.getAddress())).to.equal(ethers.parseEther("50"));
expect(await staking.nativeContract.getTotalStaked()).to.equal(ethers.parseEther("50"));
// Test failing to unstake more tokens than staked
await expect(staking.nativeContract.connect(addr1).unstake(ethers.parseEther("100")))
.to.be.revertedWith('Insufficient staked amount');
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
部署脚本
如你所见,该脚本包含部署合约和调用函数的代码。并且基于智能合约逻辑,它期望输出。
但此外,你可以看到在调用智能合约上的任何函数时,该函数实际上是在 nativeContract
上调用的,而不是在合约名称变量/实例上调用的。这是因为一旦你配置 Hardhat 以使用 Tenderly,这些合约的类型将从 BaseContract
类型更改为 TdlyContract
。你可以通过打印 BaseContract
中存在的合约实例(在 nativeContract
中)来检查这一点。通常,当未配置 Tenderly 时,它可以直接使用。但是在这里,直接调用该函数在使用 Tenderly 设置时不起作用。
设置 Tenderly 配置后的合约实例
此外,在部署后在合约上使用 waitForDeployment()
很有帮助(如上面的脚本所示),因为它将在 Tenderly TestNets 中验证合约。
由于自动合约验证设置为 true,你可以只使用如下命令运行部署脚本:其中 virtualMainnet
是 Hardhat 配置文件中的网络名称:
npx hardhat run scripts/deploy.ts --network virtualMainnet
打开 Tenderly TestNet 仪表板,然后单击事务哈希以调试事务。
点击交易哈希
然后我们开始调试:
调试 stake() 调用。
总的来说,本文给出了关于从 Tenderly TestNets 开始并调试本地项目的想法。显然,Tenderly 方面有很多内容要介绍。但是,本文更多的是记录我在编写脚本时发现的怪癖,例如正确访问基础合约对象以调用函数,我发现这很难在文档中的任何地方找到。
- 原文链接: calibersec.com/debugging...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!