Foundry 高级作弊码系列:第二部分 - 作弊码 vm.prank,模拟调用

本文介绍了 Foundry 中 vm.prank cheatcode 的使用方法,该 cheatcode 允许开发者模拟任何 msg.sender,从而测试需要特定权限才能访问的合约逻辑,例如访问控制、多重签名和元交易路径。

学习如何使用 Foundry 的 vm.prank 来模拟任何 msg.sender 并测试有权限的合约逻辑。对于访问控制、多重签名和 meta-tx 路径至关重要。

高级 Foundry Cheatcodes 系列:第 2 部分 - Cheatcode vm.prank,模拟调用

2025-06-18 - 5 分钟阅读

作者:Simeon Cholakov

Web3 安全

threesigma's twitterthreesigma's linkedinthreesigma's github

简介

很高兴你回来参加 Foundry Cheatcodes 系列的第二期。在第 1 部分中,我们将 Foundry 与 Hardhat 进行了比较,并为更快的、Solidity 原生的测试奠定了基础;今天,我们将聚焦于 vm.prank,这个技巧可以让你的测试伪装成任何地址,从而探测访问控制的边缘情况。

是否曾经需要测试访问控制或模拟来自不同用户的调用?Foundry 的 vm.prank 可以非常简单地模拟任何地址并验证你的合约逻辑。

什么是 vm.prank 以及为什么它很重要

vm.prank(address) cheatcode 使下一个合约调用看起来来自不同的地址(它设置了 msg.sender)。 这在测试依赖于调用者的逻辑时至关重要。 例如,如果只有所有者才能调用某个函数,则你可以恶搞一个非所有者并验证它是否失败。 在 Foundry 的 forge-std Test 合约中,你只需执行以下操作:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";

contract PrankExample is Test {
    address owner = address(0x1);
    address attacker = address(0x2);

    function testPrank() public {
        // 设置 attacker 为后续调用的 msg.sender
        vm.prank(attacker);

        // 现在,任何后续的交易都将看起来像是从 attacker 发起的
        // 例如,调用一个只有 owner 才能调用的函数,将会失败
    }
}

演练:使用 vm.prank 进行访问控制测试

让我们通过一个实际的例子来演示 vm.prank。假设你有一个简单的银行合约,只有所有者才能提取资金。vm.prank 允许我们模拟非所有者并确保他们无法提取资金。

步骤 1:设置 Bank 合约

首先,我们创建一个基本的 Bank 合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Bank {
    address public owner;
    uint256 public balance;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    function deposit() public payable {
        balance += msg.value;
    }

    function withdraw(uint256 amount) public onlyOwner {
        require(balance >= amount, "Insufficient balance");
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed");
        balance -= amount;
    }
}

步骤 2:编写测试用例

现在,编写一个 Foundry 测试以验证只有所有者才能提取资金:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Bank} from "../src/Bank.sol";

contract BankTest is Test {
    Bank public bank;
    address owner = address(0x1);
    address attacker = address(0x2);

    function setUp() public {
        // 将 owner 设置为部署者,并使用 owner 的私钥部署合约
        vm.deal(owner, 10 ether);
        vm.prank(owner);
        bank = new Bank();
    }

    function testWithdrawSuccess() public {
        // 给 bank 存入一些钱
        vm.deal(address(bank), 1 ether);

        // 确认 owner 可以取款
        vm.prank(owner);
        bank.withdraw(0.5 ether);
        assertEq(address(bank).balance, 0.5 ether, "Withdrawal failed");
    }

    function testWithdrawFail() public {
        // 给 bank 存入一些钱
        vm.deal(address(bank), 1 ether);

        // 尝试模拟一个非所有者取款应该会失败
        vm.prank(attacker);
        vm.expectRevert("Only owner can call this function");
        bank.withdraw(0.5 ether);
    }
}

步骤 3:说明

  • setUp 函数:将所有者设置为 address(0x1),并使用该地址部署 Bank 合约。
  • testWithdrawSuccess 函数:验证所有者是否可以成功取款。我们使用 vm.prank(owner) 来确保调用来自所有者。
  • testWithdrawFail 函数:尝试使用 vm.prank(attacker) 模拟由非所有者进行的取款。我们期望调用会因为 onlyOwner 修饰符而回滚,我们使用 vm.expectRevert 来验证这一点。

高级用法

vm.startPrankvm.stopPrank 结合使用

对于一些复杂的情况,你可能需要为一个区块范围内模拟一个 msg.sendervm.startPrankvm.stopPrank 简化了这一点。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Bank} from "../src/Bank.sol";

contract BankTest is Test {
    Bank public bank;
    address owner = address(0x1);
    address attacker = address(0x2);
    address randomUser = address(0x3);

    function setUp() public {
        // 将 owner 设置为部署者,并使用 owner 的私钥部署合约
        vm.deal(owner, 10 ether);
        vm.prank(owner);
        bank = new Bank();
    }

    function testMultipleCallsWithPrank() public {
        // 给 bank 存入一些钱
        vm.deal(address(bank), 3 ether);

        // 启动一个从 attacker 模拟的 prank
        vm.startPrank(attacker);

        // 即使我们模拟了 attacker,这个调用仍然有效,因为它是 msg.sender
        // 这是因为存款不使用 onlyOwner 修饰符
        bank.deposit{value: 1 ether}();

        // 验证余额是否正确
        assertEq(address(bank).balance, 4 ether, "Deposit failed");

        // 停止 prank
        vm.stopPrank();

        // 尝试模拟一个非所有者取款应该会失败
        vm.prank(randomUser);
        vm.expectRevert("Only owner can call this function");
        bank.withdraw(0.5 ether);
    }
}

在此示例中,vm.startPrank(attacker) 意味着所有后续调用都将看起来来自攻击者,直到使用 vm.stopPrank() 为止。

结论

vm.prank 是一个强大的工具,可用于在 Foundry 中进行访问控制测试。通过模拟不同的地址,你可以确保你的合约按照预期运行并避免未经授权的操作。

在接下来的文章中,我们将介绍 vm.deal,它允许你直接设置任何地址的 ETH 余额。

关于作者

Simeon 是一位对 Web3 安全、区块链技术和零知识证明充满热情的区块链安全工程师。在 Twitter (@threesigmaxyz) 或 Linkedin (threesigma) 上与他联系。

  • 原文链接: threesigma.xyz/blog/web3...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Three Sigma
Three Sigma
Three Sigma is a blockchain engineering and auditing firm focused on improving Web3 by working closely with projects in the space.