使用 Tenderly 调试 Hardhat 智能合约项目

本文介绍了如何将 Tenderly 集成到 Hardhat 项目中,以调试智能合约,并提供了一个使用 Tenderly 进行调试的示例,包括 Staking 和 Rewards 合约的部署和交互。文章还分享了在使用 Tenderly 时可能遇到的问题和解决方法,例如如何正确访问合约对象以调用函数,以及如何配置 Hardhat 以使用 Tenderly 进行合约验证。

请继续关注关于区块链桥接中常见漏洞、我的审计思维模型等内容的资源 ✨🔒

在本文中,我们将探讨如何将 Tenderly 与一个实现了 staking(质押)和奖励系统的示例 Hardhat 项目集成。

虽然 Tenderly 的文档会帮助你完成整个过程,但我相信我可以给你更多关于哪里可能出错的想法。

在此之前,让我们检查一下为什么需要它:

从安全角度来看,在审计协议时,你会遇到多种情况,其中跟踪控制流中各种变量值是不可行的。例如,可能会有多个嵌套调用,并且在每个调用帧中更新值。在许多大型项目中,凭空跟踪所有内容是不切实际的,并且如果你使用的是 Hardhat 项目,则也无法进行直接调试。

虽然我们已经探索了一些 本地选项。这将是一个更加用户友好的选择。可以通过共享 TestNet 进度来和多个团队协作。

项目概览

该项目由三个主要的简化智能合约组成:

  1. Token.sol:用于质押的 ERC20 代币。
  2. Staking.sol:允许用户质押代币并管理其质押金额的合约。
  3. Rewards.sol:具有奖励逻辑的合约。

对于提到的合约,合约功能 / 入口点的简短说明:

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 合约的功能性函数(Staking.sol

  1. constructor(MyToken _token, Rewards _rewards)
    • 使用 MyTokenRewards 合约地址初始化 staking 合约。
  2. function stake(uint256 amount) public nonReentrant
    • 允许用户质押指定数量的 MyToken 代币,更新其质押金额和总质押金额。
  3. function unstake(uint256 amount) public nonReentrant
    • 允许用户取消质押指定数量的 MyToken 代币,更新其质押金额、总质押金额和领取奖励。
  4. function getStakedAmount(address staker) public view returns (uint256)
    • 返回指定用户的质押金额。
  5. function getTotalStaked() public view returns (uint256)
    • 返回合约中质押的代币总数。

Rewards 合约的功能性函数(Rewards.sol

  1. function initialize(address _staking) external
    • 使用 staking 合约的地址初始化 rewards 合约。
  2. function calculateRewards(address staker) public view returns (uint256)
    • 根据指定用户的质押金额计算其奖励。
  3. function claimRewards(address staker) external
    • 领取指定用户的奖励,确保只有 staking 合约可以调用此函数。

从现在开始,你可以按照官方文档,但我还有一些你可能在文档中找不到的额外内容想要分享。

设置 Tenderly:

  1. 安装 Tenderly CLI
brew tap tenderly/tenderly
brew install tenderly
tenderly login

登录后,你可以检查 tenderly whoami 以确认。

  1. 安装 tenderly-hardhat 插件

npm install --save-dev @tenderly/hardhat-tenderly

  1. 将插件导入到你的 hardhat.config.js/ts 文件中,例如 const tenderly = require("@tenderly/hardhat-tenderly");。导入方式会因 .ts 或 .js 文件而异。
  2. 通常,如果你使用的 @tenderly/hardhat-tenderly 版本在 2.4.0 以下,你需要调用 tdly.setup() 并将 automaticVerifications 选项设置为 true,如下所示:tenderly.setup({ automaticVerifications: true }); 但如果你使用的版本高于此版本,则可以将 TENDERLY_AUTOMATIC_VERIFICATION 环境变量设置为 true,例如使用 .env 文件。否则,你可能会收到如下警告。

  1. 在 hardhat 配置文件中添加 tenderly 配置:

你需要在 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;

使用 Tenderly 部署和调试:

编写部署脚本。脚本的工作方式未进行解释,因为假设读者已经知道它了。

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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
calibersec
calibersec
江湖只有他的大名,没有他的介绍。