Skip to main content

validationIntersection

vm.startPrank

在智能合约测试中,vm.startPrank 是 Foundry 测试框架中的一个功能,它用于模拟区块链上的特定地址的行为。当你调用 vm.startPrank 并传递一个地址时,你告诉测试框架模拟该地址的行为,直到调用 vm.stopPrank

这意味着在 vm.startPrankvm.stopPrank 之间的所有操作都会被执行,就好像它们是由 vm.startPrank 传递的地址发起的一样。这对于测试需要特定权限或角色的函数非常有用,例如,仅允许合约的所有者调用的函数。

以下是 vm.startPrank 的一些典型用途:

  1. 模拟交易发送者: 你可以模拟一个特定用户或合约地址发送交易,以测试权限控制逻辑。

  2. 状态隔离: 在模拟的地址上执行的所有状态更改(例如,以太币余额的变化、合约状态变量的变化等)在 vm.stopPrank 调用之后不会影响后续的测试。

  3. 行为连贯性: 一系列连续的调用可以在同一个模拟的地址上下文中执行,而不需要每次调用都指定发送者。

在你提供的代码中,vm.startPrank(address(owner1)); 表示开始模拟地址 owner1 的行为,直到 vm.stopPrank() 被调用。这在安装插件时被使用,因为只有账户的所有者才能安装插件到他们自己的账户上。

为什么其他单元测试没有使用 vm.startPrank?

在单元测试中,是否使用 vm.startPrank 取决于测试的特定需求。vm.startPrank 主要用于模拟一个特定地址的行为,这在需要测试基于地址的权限控制时非常有用。如果某个功能需要特定的权限或角色(例如,只有所有者可以执行的操作),则使用 vm.startPrank 可以确保测试从该特定地址的角度进行。

在其他单元测试中可能没有使用 vm.startPrank 的原因可能有:

  1. 不需要权限控制:如果测试的函数不涉及权限控制,或者不需要模拟特定地址的行为,那么就不需要使用 vm.startPrank

  2. 全局影响:如果测试的函数对所有用户都有相同的效果,或者不需要模拟特定的用户交互,那么使用 vm.startPrank 就不是必需的。

  3. 单次交易:如果只需要发送一次交易,并且不需要维持状态或连续性,可以直接使用 vm.prank,它允许发送单个交易作为指定地址,而不需要使用 vm.startPrankvm.stopPrank 包围一系列操作。

  4. 默认发送者:在某些测试框架中,默认的发送者可能已经是合约的所有者或具有必要权限的地址,因此不需要额外的模拟。

  5. 测试框架的功能:不同的测试框架可能提供不同的方法来处理权限和模拟地址。有些框架可能有替代 vm.startPrank 的方法来模拟地址或设置权限。

因此,在你看到的其他单元测试中,如果没有使用 vm.startPrank,那么这些测试可能不需要模拟特定地址的状态或权限,或者可能使用了其他方法来处理这些需求。

测试 eth 发送

    function test_postDeploy_ethSend() public {
UserOperation memory userOp = UserOperation({
sender: address(account2),
nonce: 0,
initCode: "",
callData: abi.encodeCall(UpgradeableModularAccount.execute, (ethRecipient, 1 wei, "")),
callGasLimit: CALL_GAS_LIMIT,
verificationGasLimit: VERIFICATION_GAS_LIMIT,
preVerificationGas: 0,
maxFeePerGas: 1,
maxPriorityFeePerGas: 1,
paymasterAndData: "",
signature: ""
});

// Generate signature
bytes32 userOpHash = entryPoint.getUserOpHash(userOp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash());
userOp.signature = abi.encodePacked(r, s, v);

UserOperation[] memory userOps = new UserOperation[](1);
userOps[0] = userOp;

entryPoint.handleOps(userOps, beneficiary);

assertEq(ethRecipient.balance, 2 wei);
}

这个是干嘛的

这段代码是一个智能合约的单元测试函数,它的目的是验证一个用户操作(UserOperation)是否能够成功地将以太币发送到指定的接收者(ethRecipient)。这个测试是在一个基于以太坊的区块链环境中进行的,其中使用了一些专门的测试工具和方法。

下面是代码的逐行解释:

  1. 定义一个名为 test_postDeploy_ethSend 的函数,这是一个公共函数,意味着它可以在合约外部调用。

  2. 创建一个 UserOperation 结构体实例 userOp,它包含了执行一个用户操作所需的所有信息:

    • sender 是操作的发起者,这里设置为 account2 的地址。
    • nonce 是操作的序号,用于防止重放攻击,这里设置为 0。
    • initCode 是创建合约时执行的代码,这里为空字符串,表示不创建新合约。
    • callData 包含了调用 UpgradeableModularAccount.execute 函数的编码数据,它会向 ethRecipient 地址发送 1 wei。
    • callGasLimitverificationGasLimitpreVerificationGas 是与操作执行和验证相关的气体限制和成本。
    • maxFeePerGasmaxPriorityFeePerGas 是用户愿意为每单位气体支付的最大费用。
    • paymasterAndData 是支付操作费用的合约(如果有)及其数据,这里为空字符串。
    • signature 是操作的签名,初始为空。
  3. 生成操作的哈希值 userOpHash,这个哈希是用于签名的消息。

  4. 使用 vm.sign 函数和私钥 owner2KeyuserOpHash 进行签名,以证明 account2 授权了这个操作。签名由三部分组成:vrs

  5. 将签名的组成部分编码并设置到 userOp.signature 中。

  6. 创建一个包含单个 UserOperation 的数组 userOps

  7. 通过调用 entryPoint.handleOps 函数来处理 userOps,其中 beneficiary 是在操作成功后接收手续费的地址。

  8. 使用 assertEq 函数来断言 ethRecipient 的余额是否为 2 wei,以验证操作是否成功将 1 wei 发送给了 ethRecipient

总之,这个测试函数模拟了一个用户操作,该操作通过 UpgradeableModularAccount.execute 函数发送一定量的以太币给接收者,并验证了操作的执行是否符合预期。这是智能合约开发中确保合约行为正确性的标准做法。

测试升级模块化账户的存储访问

function test_debug_upgradeableModularAccount_storageAccesses() public {
UserOperation memory userOp = UserOperation({
sender: address(account2),
nonce: 0,
initCode: "",
callData: abi.encodeCall(UpgradeableModularAccount.execute, (ethRecipient, 1 wei, "")),
callGasLimit: CALL_GAS_LIMIT,
verificationGasLimit: VERIFICATION_GAS_LIMIT,
preVerificationGas: 0,
maxFeePerGas: 1,
maxPriorityFeePerGas: 1,
paymasterAndData: "",
signature: ""
});

// Generate signature
bytes32 userOpHash = entryPoint.getUserOpHash(userOp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(owner2Key, userOpHash.toEthSignedMessageHash());
userOp.signature = abi.encodePacked(r, s, v);

UserOperation[] memory userOps = new UserOperation[](1);
userOps[0] = userOp;

vm.record();
entryPoint.handleOps(userOps, beneficiary);
_printStorageReadsAndWrites(address(account2));
}

这个是干嘛的?

这段代码是一个智能合约的单元测试函数,其目的是验证一个可升级模块化账户(UpgradeableModularAccount)在处理用户操作(UserOperation)时的存储访问情况。特别地,它检查了在执行用户操作时,相关智能合约的存储读取和写入行为。

以下是代码的详细解释:

  1. 定义一个名为 test_debug_upgradeableModularAccount_storageAccesses 的公共测试函数。

  2. 创建一个 UserOperation 结构体实例 userOp,用于描述一个用户操作。这里的字段与之前的代码段相同,包括发起者、nonce、调用数据等。

  3. 通过 entryPoint.getUserOpHash 函数生成用户操作的哈希值 userOpHash

  4. 使用 vm.sign 函数和私钥 owner2KeyuserOpHash.toEthSignedMessageHash() 结果进行签名,以模拟操作发起者的签名过程。签名结果由 v, r, s 三部分组成。

  5. 将签名的组成部分编码到 userOp.signature 字段中。

  6. 创建一个只包含一个元素的 UserOperation 数组 userOps,并将之前创建的 userOp 作为唯一元素。

  7. 调用 vm.record() 函数开始记录智能合约的存储访问行为。这是测试环境中的一个特殊功能,用于追踪合约执行期间的存储读写操作。

  8. 通过 entryPoint.handleOps 函数处理用户操作数组 userOps,同时传入 beneficiary 作为在操作成功后接收手续费的地址。

  9. 调用自定义的 _printStorageReadsAndWrites 函数,传入 account2 的地址,打印出在执行用户操作期间对 account2 的存储访问情况。

整个测试函数的目的是通过记录和检查智能合约在处理特定操作时的存储访问模式,来帮助开发者调试和验证合约的存储访问是否按照预期进行。这可以帮助识别潜在的存储相关的问题,如不必要的存储读写,或是存储访问权限的问题等。在复杂的智能合约系统中,这种测试对于合约性能优化和安全性检查非常有用。

ExecuteFromPlugin,未安装允许的 ExecSelector

    function test_installPlugin_ExecuteFromPlugin_PermittedExecSelectorNotInstalled() public {
vm.startPrank(owner2);

PluginManifest memory m;
m.permittedExecutionSelectors = new bytes4[](1);
m.permittedExecutionSelectors[0] = IPlugin.onInstall.selector;

MockPlugin mockPluginWithBadPermittedExec = new MockPlugin(m);
bytes32 manifestHash = keccak256(abi.encode(mockPluginWithBadPermittedExec.pluginManifest()));

IPluginManager(account2).installPlugin({
plugin: address(mockPluginWithBadPermittedExec), // 安装一个没有权限执行的插件
manifestHash: manifestHash,
pluginInstallData: "",
dependencies: new FunctionReference[](0)
});
}

test_installPlugin_ExecuteFromPlugin_PermittedExecSelectorNotInstalled 这个测试函数的名称暗示了它的测试目的:验证在安装插件时,如果一个插件声明了某个允许执行的选择器(permittedExecutionSelectors),但这个选择器实际上并没有被安装或者在系统中注册,系统的行为是如何的。

在智能合约系统中,特别是在一个插件化的架构中,插件可能会声明它们需要使用的函数选择器。这些选择器标识了可以被安全地从插件中调用的合约函数。如果一个插件声明了一个选择器,但是系统中没有相应的实现,可能会出现安全问题或者功能不完整的问题。

这个测试可能会检查以下情况之一:

  • 确保系统能够在没有安装声明的选择器的情况下,拒绝安装该插件。
  • 确保系统在安装了声明了不存在选择器的插件后,不会允许从该插件执行未注册的选择器。
  • 确保系统能够在安装了声明了不存在选择器的插件后,正确地报告错误或者处理这种情况。

从测试函数的名称来看,它可能是专门设计来确保系统在遇到未安装的执行选择器时能够正确处理,并保持系统的安全性和稳定性。然而,没有看到测试函数的具体实现,我们无法确定它具体检查了哪些断言或行为。通常,这样的测试函数会包含一些断言(assertions),用来验证合约的实际行为是否符合预期的行为。

test_uninstallPlugin_manifestParameter

function test_uninstallPlugin_manifestParameter() public {
vm.startPrank(owner2);

ComprehensivePlugin plugin = new ComprehensivePlugin();
bytes memory serializedManifest = abi.encode(plugin.pluginManifest());
bytes32 manifestHash = keccak256(serializedManifest);
IPluginManager(account2).installPlugin({
plugin: address(plugin),
manifestHash: manifestHash,
pluginInstallData: "",
dependencies: new FunctionReference[](0)
});

vm.expectEmit(true, true, true, true);
emit PluginUninstalled(address(plugin), true);
IPluginManager(account2).uninstallPlugin({
plugin: address(plugin),
config: serializedManifest,
pluginUninstallData: ""
});
address[] memory plugins = IAccountLoupe(account2).getInstalledPlugins();
assertEq(plugins.length, 1);
assertEq(plugins[0], address(singleOwnerPlugin));
}

是的,test_uninstallPlugin_manifestParameter 这个测试函数的名称表明它是用来测试与卸载插件相关的参数,特别是传递给卸载函数的插件清单(manifest)参数。在智能合约的测试中,通常会有一系列的测试函数,每个函数专注于测试合约的不同方面或不同的功能。

在这个特定的测试中,serializedManifest 参数被传递给了 uninstallPlugin 方法,这个参数是插件清单的序列化版本。测试的目的是确保当这个参数被传递到卸载方法时,合约能够正确地处理它,并且卸载过程能够成功执行。

测试的步骤包括:

  1. 模拟 owner2 执行操作。
  2. 实例化一个 ComprehensivePlugin 对象。
  3. 编码插件的清单并计算其哈希值。
  4. 安装插件。
  5. 预期发出 PluginUninstalled 事件。
  6. 实际卸载插件。
  7. 检查卸载后的状态,确保插件已经从已安装的插件列表中移除。

这个测试函数中,serializedManifest 参数是关键,因为它提供了卸载操作所需的配置信息。通过传递这个参数,测试可以验证智能合约是否能够正确地解析和使用这些数据来卸载插件。如果合约能够正确地处理这个参数并成功卸载插件,那么测试就通过了。