本文介绍了Foundry测试框架中的vm.prank
作弊码,它允许开发者模拟任何地址作为msg.sender
,从而方便测试需要权限控制的合约逻辑,例如模拟不同的用户调用合约,测试访问控制和多重签名等场景。
学习如何使用 Foundry 的 vm.prank 来模拟任何 msg.sender 并测试有权限的合约逻辑。 对于访问控制、多重签名和 meta-tx 路径至关重要。
2025-06-18 - 5 分钟阅读
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
。它模拟设置 address
为 msg.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
模拟了两个所有者(owner1
和 owner2
)签署交易,满足了所需的签名阈值,以便成功执行,而 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!