Foundry作弊码第二部分:使用vm.prank模拟任何地址

本文介绍了Foundry测试框架中的vm.prank作弊码,它允许开发者模拟任何地址作为msg.sender,从而方便测试需要权限控制的合约逻辑,例如模拟不同的用户调用合约,测试访问控制和多重签名等场景。

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

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

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

2025-06-18 - 5 分钟阅读

作者:Simeon Cholakov

Foundry

threesigma's twitterthreesigma's linkedinthreesigma's github

服务
支持
信息

介绍

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

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

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

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

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

import "forge-std/Test.sol";

contract MyContractTest is Test {
    function testPrank() public {
        address attacker = address(0x420); // 定义一个攻击者地址
        vm.prank(attacker);                // 下一个调用将来自攻击者
        // ... call your contract function here
    }
}

vm.prank 接受一个参数,你希望在后续的调用中使用的 address。它模拟设置 addressmsg.sender(对外部拥有的账户 EOA 和合约都有效),允许完全控制调用上下文。

用例概述

  • 访问控制测试vm.prank 允许你验证只有授权账户才能执行敏感操作,并且未经授权的尝试会被拒绝。
  • 多重签名模拟:在多重签名方案中,你可以模拟来自多个所有者的交易,以确保收集到足够的签名才能执行操作。
  • 元交易测试:如果你正在处理元交易,vm.prank 允许你模拟代表用户执行交易的中继器,从而确保它们的行为符合预期。

访问控制测试

让我们深入研究 vm.prank 在访问控制上下文中的实际使用。考虑一个带有 transfer 函数的简单的 ERC20 合约,该函数只能由合约的所有者调用。

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

contract ERC20 {
    address public owner;
    mapping(address => uint256) public balances;

    constructor(address _owner) {
        owner = _owner;
    }

    function transfer(address _to, uint256 _amount) public {
        require(msg.sender == owner, "Only owner can transfer");
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }
}

以下是如何使用 vm.prank 测试访问控制:

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

import "forge-std/Test.sol";

contract ERC20Test is Test {
    ERC20 erc20;
    address alice = makeAddr("alice");
    address bob = makeAddr("bob");

    function setUp() public {
        erc20 = new ERC20(address(this));
        erc20.balances(address(this)) == 1000 ether;
    }

    function testTransfer_Success() public {
        // 验证所有者是否可以转移
        vm.prank(address(this));
        erc20.transfer(alice, 100 ether);
        assertEq(erc20.balances(alice), 100 ether);
    }

    function testTransfer_Failure() public {
        // 验证未经授权的地址是否无法转移
        vm.prank(alice);
        vm.expectRevert("Only owner can transfer");
        erc20.transfer(bob, 100 ether);
    }
}

testTransfer_Success 中,vm.prank(address(this)) 模拟所有者执行转移,从而使其成功。相反,testTransfer_Failure 使用 vm.prank(alice) 模拟未经授权的地址,期望事务恢复,因为调用者不是所有者。

多重签名模拟

vm.prank 也适用于多重签名场景。假设你有一个需要多个签名才能执行交易的多重签名合约。你可以使用 vm.prank 模拟来自不同所有者的签名来满足所需的阈值。

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

contract MultiSig {
    address[] public owners;
    uint256 public requiredSignatures;
    mapping(bytes32 => mapping(address => bool)) public signatures;
    mapping(bytes32 => bool) public executed;

    constructor(address[] memory _owners, uint256 _requiredSignatures) {
        owners = _owners;
        requiredSignatures = _requiredSignatures;
    }

    function submitTransaction(address _to, uint256 _value, bytes memory _data) public returns (bytes32 transactionId) {
        transactionId = keccak256(abi.encode(_to, _value, _data));
        require(!executed[transactionId], "Transaction already executed");
        require(isOwner(msg.sender), "Not an owner");
        signatures[transactionId][msg.sender] = true;
    }

    function confirmTransaction(bytes32 transactionId) public {
        require(!executed[transactionId], "Transaction already executed");
        require(isOwner(msg.sender), "Not an owner");
        signatures[transactionId][msg.sender] = true;
    }

    function executeTransaction(address _to, uint256 _value, bytes memory _data) public {
        bytes32 transactionId = keccak256(abi.encode(_to, _value, _data));
        require(!executed[transactionId], "Transaction already executed");
        require(countSignatures(transactionId) >= requiredSignatures, "Not enough signatures");
        executed[transactionId] = true;
        (bool success, ) = _to.call{value: _value}(_data);
        require(success, "Transaction failed");
    }

    function isOwner(address _address) public view returns (bool) {
        for (uint256 i = 0; i < owners.length; i++) {
            if (owners[i] == _address) {
                return true;
            }
        }
        return false;
    }

    function countSignatures(bytes32 transactionId) public view returns (uint256) {
        uint256 count = 0;
        for (uint256 i = 0; i < owners.length; i++) {
            if (signatures[transactionId][owners[i]]) {
                count++;
            }
        }
        return count;
    }
}

这是 Foundry 中测试多重签名合约的方法:

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

import "forge-std/Test.sol";

contract MultiSigTest is Test {
    MultiSig multiSig;
    address owner1 = makeAddr("owner1");
    address owner2 = makeAddr("owner2");
    address owner3 = makeAddr("owner3");
    address[] owners;
    uint256 requiredSignatures = 2;
    address payable receiver = payable(makeAddr("receiver"));

    function setUp() public {
        owners = [owner1, owner2, owner3];
        multiSig = new MultiSig(owners, requiredSignatures);
    }

    function testExecuteTransaction_Success() public {
        // 所有者 1 提交交易
        vm.prank(owner1);
        bytes32 transactionId = multiSig.submitTransaction(receiver, 1 ether, "");
        // 所有者 2 确认交易
        vm.prank(owner2);
        multiSig.confirmTransaction(transactionId);
        // 执行交易
        multiSig.executeTransaction(receiver, 1 ether, "");
        // 检查是否成功
        assertTrue(address(receiver).balance == 1 ether);
    }

    function testExecuteTransaction_InsufficientSignatures() public {
        // 所有者 1 提交交易
        vm.prank(owner1);
        bytes32 transactionId = multiSig.submitTransaction(receiver, 1 ether, "");
        // 试图在没有足够签名的情况下执行交易
        vm.expectRevert("Not enough signatures");
        multiSig.executeTransaction(receiver, 1 ether, "");
    }
}

在这个测试套件中,testExecuteTransaction_Success 模拟了两个所有者(owner1owner2)签署交易,满足了所需的签名阈值,以便成功执行,而 testExecuteTransaction_InsufficientSignatures 尝试在没有足够签名的情况下执行交易,导致恢复。

元交易测试

vm.prank 也适用于测试元交易,在元交易中,交易由代表用户的中继器提交。例如,考虑一个允许用户使用中继器执行交易的简单合约。

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

contract MetaTransaction {
    mapping(address => uint256) public nonces;

    function executeMetaTransaction(address userAddress, bytes memory functionSignature, bytes32 sigR, bytes32 sigS, uint8 sigV) public payable {
        bytes32 hash = keccak256(abi.encode(
            address(this),
            userAddress,
            nonces[userAddress],
            keccak256(functionSignature)
        ));

        address signer = ecrecover(hash, sigV, sigR, sigS);
        require(signer == userAddress, "Invalid signature");

        nonces[userAddress]++;

        (bool success, ) = address(this).call(functionSignature);
        require(success, "Transaction failed");
    }
}

你可以使用 vm.prank 模拟中继器来测试这一点:

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

import "forge-std/Test.sol";

contract MetaTransactionTest is Test {
    MetaTransaction metaTransaction;
    address user = makeAddr("user");
    address relayer = makeAddr("relayer");

    function setUp() public {
        metaTransaction = new MetaTransaction();
    }

    function testExecuteMetaTransaction() public {
        // 构建元交易
        bytes memory functionSignature = abi.encodeWithSignature("setNumber(uint256)", 42);
        bytes32 hash = keccak256(abi.encode(
            address(metaTransaction),
            user,
            0,
            keccak256(functionSignature)
        ));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(user, hash);

        // 模拟中继器调用 executeMetaTransaction
        vm.prank(relayer);
        metaTransaction.executeMetaTransaction(user, functionSignature, r, s, v);

        // 验证交易是否成功
        // assertEq(metaTransaction.number(), 42);
    }
}

在此测试中,testExecuteMetaTransaction 模拟中继器调用 executeMetaTransaction 函数,确保交易按预期执行。

结论

Foundry 的 vm.prank 作弊码是测试合约中与上下文相关的逻辑的强大工具。通过允许你模拟任何 msg.sender,你可以彻底验证访问控制机制、多重签名合约和元交易工作流程,使你的智能合约更加健壮且无错误。

下次见,继续编写出色的代码!

进一步阅读

参考文献

  • 原文链接: threesigma.xyz/blog/foun...
  • 登链社区 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.