ChainLight 修补周四 - 账户抽象安全指南

本文分析了基于ERC4337的账户抽象项目在安全审计中发现的漏洞,涉及Gas费计算、签名生成与使用、重用签名及抢跑等多个方面,探讨了这些漏洞可能导致的安全隐患,并给出了开发项目时需优先考虑的安全建议。

作者: ChainLight

来源: ChainLight 博客

总结

在上一篇文章中,我们讨论了账户抽象的组成部分和操作结构,以及相关的安全考虑。在本文中,我们将分析和分类在使用账户抽象实施的项目的安全审计中发现的漏洞。基于这些发现,我们将介绍使用账户抽象的项目应优先考虑的事项。

AA 项目的安全考虑

ChainLight 分析了基于 ERC4337 建立或计划建立的项目的安全审计报告,并根据这些报告汇总了使用账户抽象项目的基本注意事项。在本文中,我们引用了 OpenZeppelin 对 ERC4337 参考实现的审计报告,以及经过详细安全审计的 Biconomy、Zerodev Kernel 和 Ambire 等项目的其他报告。

1. Gas费计算逻辑

基于 ERC4337 的项目可以提供的一个关键特性是通过 Bundlers 代表用户支付Gas费,并让 Paymaster 在交易成功时向 Bundler 划拨已使用的费用。因此,使恶意 Bundler 操纵Gas费奖励或无辜的 Bundler 无法收到适当付款的漏洞将导致参与实体的财务损失。

Gas偿还计算错误

由于Gas费操控漏洞可能导致资金被盗,因此应谨慎处理Gas费偿还逻辑。在 Biconomy 中,账户为代替用户提交交易的 Relayer(类似于 ERC4337 的 Bundler)提供补偿,这一补偿设计与 calldata 的大小成比例。然而,一个漏洞使得攻击者或恶意 Relayers 可以通过向 calldata 添加零字节来任意增加补偿。

在这个漏洞中,当 Relayer 提交交易时,账户根据 msg.data.length(完整的 calldata)支付每个 8 Gas。但是,EVM 对于 calldata 中的零字节仅消耗 4 Gas,而对非零字节消耗 16 Gas。因此,每当 Relayer 向 calldata 添加零字节时,实际上获得 4 个额外的Gas(8 个现金补偿 - 4 个实际消耗成本)。这允许攻击者通过向 calldata 添加大量零字节来获取额外的补偿,消耗账户的资产。这一问题在 Infinitism 的 ERC4337 标准实现发布之前就已存在。在引用的实现中,补偿 Bundler 的费用由 UserOperation 的 maxFeePerGasmaxPriorityFeePerGaspreVerificationGas 参数决定,因此此类操控现在变得不可行。然而,设置这些参数时必须谨慎,如果设置得太高,可能会导致用户账户向 Bundler 支付不必要的高Gas补偿。

Bundler 的Gas估算错误

如果 Bundler 在模拟期间进行的Gas估算低于在链上执行所需的实际Gas成本,则当使用预计的Gas量执行交易时,会因超出Gas用量而导致交易失败。在 Infinitism 的 ERC4337 之前的实现中,由于在模拟阶段访问冷存储,导致模拟阶段的Gas估算低于链上环境下的实际执行。此漏洞中,Bundler 在调用 _validateAccountPrepayment() 时对地址聚合器传入的值为 1。在这个过程中,Bundler 从发送方账户的存储中检索发送方账户聚合器的地址。由于在模拟阶段首次访问发送方账户的存储(冷访问),在调用发送者账户的 UserOperation 验证时调用 validateUserOp() 产生的Gas成本较低(这被称作温访问),导致模拟和实际执行环境之间的Gas使用差异,因此交易可能因链上执行时的Gas不足而失败。为防止这种情况,Bundlers 必须确保模拟环境和链上执行之间的Gas测量没有差异,尤其是在直接设计 EntryPoint 时。

2. 签名生成与使用

正确生成和验证签名确实至关重要。如果签名验证未正确执行,攻击者可能会冒充账户并执行任意交易,可能导致严重的用户资金盗窃问题。

对生成签名的检查不足

在 Biconomy 的 SmartAccount 合约中,存在一个漏洞,攻击者可以创造任意交易绕过签名验证。SmartAccount 合约通过 execTransaction() 函数支持基于 EIP1271 的签名交易。不过,它缺少一个验证过程来检查 checkSignatures() 函数中的输入签名是否属于合约所有者。

// https://github.com/code-423n4/2023-01-biconomy/blob/8a7b05ac58a65727e7e1fb17b91e418bc372be2b/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L302-L353
function checkSignatures (…) {
  …
  _signer = address(uint160(uint256(r)));
  require(ISignatureValidator(_signer).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "BSA024");
  …
}

由于签名方未验证是否是账户所有者,攻击者可以为签名指定攻击合约地址,使得 isValidSignature() 始终返回 EIP1271_MAGIC_VALUE。因此,攻击者可以创建任意交易并通过账户执行。这可能导致从账户盗取资金、通过 selfdestruct() 销毁代理合约以及通过更改实现合约来控制钱包。攻击者可能部署新的实现以窃取账户中的所有资金,甚至从协议中撤回资金。由于 ERC4337 基于账户的签名验证错误可能导致资金被盗,因此需要特别关注确保签名验证中没有错误。

Infinitism 的实施在签名验证机制中存在问题。BLSSignatureAggregator 合约提供了一个机制,Bundler 在组装捆绑之前验证每个 UserOperation 的签名。然后,Bundler 在链下生成成功 UserOperation 的组合签名,随后可以在链上环境的 EntryPoint 中一次性验证。然而,有一个漏洞使得个别签名的验证通过,但组合签名的验证失败。

下面的 validateUserOpSignature() 在代码中调用 getUserOpPublicKey() 获取 UserOperation 的 publicKeyGetUserOpPublicKey() 根据是否存在 initCode 检索 publicKey。如果两个 publicKey 不同,组合 UserOp 的 验证 可能失败,即个别 UserOp 的验证成功。在这种情况下,Bundler 无法获取Gas费用,因为提交的交易因验证失败而回退。此外,假设聚合器已回退了 Bundler 的成功交易,这可能会损害聚合器的声誉,并导致其他利用某些聚合器的 UserOp 被拒绝。

// https://github.com/eth-infinitism/account-abstraction/blob/6dea6d8752f64914dd95d932f673ba0f9ff8e144/contracts/bls/BLSSignatureAggregator.sol#L123-L131
function validateUserOpSignature(UserOperation calldata userOp)
… (bytes memory sigForUserOp) {
  uint256[2] memory signature = abi.decode(userOp.signature, (uint256[2]));
  uint256[4] memory pubkey = getUserOpPublicKey(userOp);
  uint256[2] memory message = userOpToMessage(userOp);
  require(BLSOpen.verifySingle(signature, pubkey, message), "BLS: wrong sig");
  return "";
}

// https://github.com/eth-infinitism/account-abstraction/blob/6dea6d8752f64914dd95d932f673ba0f9ff8e144/contracts/bls/BLSSignatureAggregator.sol#L20-L27
function getUserOpPublicKey(UserOperation memory userOp) … (uint256[4] memory publicKey) {
  bytes memory initCode = userOp.initCode;
  if (initCode.length > 0) {
    publicKey = getTrailingPublicKey(initCode);
  } else {
    return IBLSAccount(userOp.sender).getBlsPublicKey();
  }
}

使用未签名变量

Biconomy 的 SmartAccount 合约有一个 handlePayment() 函数,该函数使账户向提交交易的 Relayer 支付Gas费。该函数使用与Gas相关的变量,如 baseGasgasPricegasToken,这些变量与账户支付的Gas数量密切相关。然而,存在一个漏洞,允许 Relayer 任意设置参数之一 tokenGasPriceFactor。最终账户必须偿还给 Bundler 的付款由 tokenGasPriceFactor 决定,如在 handlePayment() 代码中所示。攻击者可以操控 tokenGasPriceFactor 的值,向账户收取超过实际Gas成本的费用,从而可能导致资金被盗。

// https://github.com/code-423n4/2023-01-biconomy/blob/8a7b05ac58a65727e7e1fb17b91e418bc372be2b/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L247-L269
function handlePayment(
  uint256 gasUsed,
  uint256 baseGas,
  uint256 gasPrice,
  uint256 tokenGasPriceFactor,
  address gasToken,
  address payable refundReceiver
) private nonReentrant returns (uint256 payment) {
  // uint256 startGas = gasleft();
  // solhint-disable-next-line avoid-tx-origin
  address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver;
  if (gasToken == address(0)) {
    // 对于 ETH,我们只会将Gas价格调整到不高于实际使用的Gas价格
    payment = (gasUsed + baseGas) * (gasPrice < tx.gasprice ? gasPrice : tx.gasprice);
    (bool success,) = receiver.call{value: payment}("");
    require(success, "BSA011");
  } else {
    payment = (gasUsed + baseGas) * (gasPrice) / (tokenGasPriceFactor);
    require(transferToken(gasToken, receiver, payment), "BSA012");
  }
  // uint256 requiredGas = startGas - gasleft();
  // console.log("hp %s", requiredGas);
}

3. 签名重用

允许在短时间内多次发送相同 UserOperation 的签名重用可能会导致支付方的资金耗尽,并显著降低参与组件的可用性。因此,基于 ERC4337 的项目必须谨慎实现对 UserOperations 和交易上签名重用的验证。

通过重用 UserOperation 签名设置任意验证器

在 Zerodev Kernel 的 KillSwitchValidator 合约中,validateUserOp() 函数验证 UserOperation 的有效性,并在指定验证器方面发挥作用。当 UserOperation 中签名的长度为 71 时,会发生设置新验证器的过程,这在下面的代码中显示:

// https://github.com/zerodevapp/kernel/blob/199ae7d838f21b37f069267c86e8eeb6f0175a69/src/validator/KillSwitchValidator.sol#L43-L80
function validateUserOp(UserOperation calldata _userOp, bytes32 _userOpHash, uint256) external payable override returns (uint256) {
  KillSwitchValidatorStorage storage validatorStorage = killSwitchValidatorStorage[_userOp.sender];
  uint48 pausedUntil = validatorStorage.pausedUntil;
  uint256 validationResult = 0;
  if (address(validatorStorage.validator) != address(0)) {
    // 首先检查验证器
    try validatorStorage.validator.validateUserOp(_userOp, _userOpHash, pausedUntil) returns (uint256 res) { // ECDSAValidator
    validationResult = res;
    } catch {
      validationResult = SIG_VALIDATION_FAILED;
    }
    ValidationData memory validationData = _parseValidationData(validationResult);
    if (validationData.aggregator != address(1)) {
      // 如果签名验证未失败,返回结果
      uint256 delayedData = _packValidationData(false, 0, pausedUntil);
      return _packValidationData(_intersectTimeRange(validationResult, delayedData));
    }
  }
  if (_userOp.signature.length == 71) {
    // 保存数据到此存储
    validatorStorage.pausedUntil = uint48(bytes6(_userOp.signature[0:6]));
    validatorStorage.validator = KernelStorage(msg.sender).getDefaultValidator();
    validatorStorage.disableMode = KernelStorage(msg.sender).getDisabledMode();
    bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(bytes.concat(_userOp.signature[0:6], _userOpHash)));
    address recovered = ECDSA.recover(hash, _userOp.signature[6:]);
    if (validatorStorage.guardian != recovered) {
      return SIG_VALIDATION_FAILED;
    }
    return _packValidationData(false, 0, pausedUntil);
  } else {
    return SIG_VALIDATION_FAILED;
  }
}

漏洞出现在 validatorStorage 的数据中,该存储关于 UserOperation 的验证器信息可以通过 msg.sender 更新。换句话说,即使 userOp.sendermsg.sender 不同,msg.sender(攻击者)也可以将 killSwitchValidatorStorage[_userOp.sender].validator 指定为他们选择的任何地址。

假设攻击者使用已使用过的 UserOperation 调用 validateUserOp()。当未指定验证器时,第一个条件 if (address(validatorStorage.validator) != address(0)) 将会通过。即使验证器已存在,它不会对已使用过的 UserOperation 触发回退,而是将 SIG_VALIDATION_FAILED 分配给 validationResult。这里关键的一点是,随后在第二个 if 条件中,攻击者可以在检查 UserOperation 的签名长度后,将攻击者指定的地址设置为 validatorStorage.validator。由于先前的 UserOperation 被重用,攻击者可以通过 validatorStorage.guardian 的条件。因此,攻击者可以利用新设置的验证器任意通过任何 UserOperation 的验证。这可能导致钱包的所有权或资金被盗。

支付方的签名重用

在 Biconomy 的 Upgrader 合约中发现了一个漏洞,攻击者可以使用 delegatecall 升级账户的实现为恶意合约。攻击者可以将账户升级为与原始 SmartAccount 合约执行相同功能的合约,而无需nonce验证。这样,攻击者可以使用他们以前在账户中使用的相同支付方签名重复发送相同的交易,从而消耗支付方的资金。对此,Biconomy 已在 EntryPoint 添加了重放攻击的检查,并通过添加 NonceManager修改合约 以处理 nonce 验证,此前的处理由账户自己完成。

使用账户抽象的项目应确保在信任某些 EntryPoints 时实施适当的 nonce 验证。此外,建议最小化支付方对账户的信任依赖。

在 Infinitism 的 ERC4337 标准实现中,发现了一个漏洞,允许支付方重用签名。VerifyingPaymaster 合约验证用户为支付方的 UserOperation,计算 UserOperation 的哈希,如下面代码所示,并对此进行签名验证。然而,签名数据未包含特定地址的支付方或链 ID,这使得可以在不同链上或与具有相同 verifyingSigner 的不同支付方上重用该数据。

// https://github.com/eth-infinitism/account-abstraction/blob/6dea6d8752f64914dd95d932f673ba0f9ff8e144/contracts/samples/VerifyingPaymaster.sol#L36-L50
function getHash(UserOperation calldata userOp) public pure returns (bytes32) {
  return keccak256(abi.encode(
    userOp.getSender(),
    userOp.nonce,
    keccak256(userOp.initCode),
    keccak256(userOp.callData),
    userOp.callGasLimit,
    userOp.verificationGasLimit,
    userOp.preVerificationGas,
    userOp.maxFeePerGas,
    userOp.maxPriorityFeePerGas
  ));
}

// https://github.com/eth-infinitism/account-abstraction/blob/6dea6d8752f64914dd95d932f673ba0f9ff8e144/contracts/samples/VerifyingPaymaster.sol#L56-L75
function validatePaymasterUserOp(…) … {
  bytes32 hash = getHash(userOp);
  …
  if (verifyingSigner != hash.toEthSignedMessageHash().recover(paymasterAndData[20 :]) {
    return ("",1);
  }
}

Infinitism 通过修订 getHash() 函数来解决此问题,以包含 block.chainid、支付方合约的地址和签名的到期时间。

签名交易重用

Biconomy 的 SmartAccount 合约中的 execTransaction() 函数负责执行交易,在发送时计算签名交易的哈希,并包括 nonces[batchId]。然而,它未验证 batchId 本身,从而允许设置任意 batchId。利用这一点,攻击者可以对不同 batchId 重用相同的交易,而共享相同的 nonce 值(nonces[batchId])。

// https://github.com/code-423n4/2023-01-biconomy/blob/53c8c3823175aeb26dee5529eeefa81240a406ba/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L192-L245
function execTransaction(
  Transaction memory _tx,
  uint256 batchId,
  bytes memory signatures
) … {
  …
  bytes32 txHash;
  {
    bytes memory txHashData =
    encodeTransactionData(
      _tx,
      refundInfo,
      nonces[batchId]
    );
    nonces[batchId]++;
    txHash = keccak256(txHashData);
    checkSignatures(txHash, txHashData, signatures);
  }
  …
}

这样,对于特定的 nonce 值,最多可以有 ²²⁵⁶ 个可能的 batchId 值,允许同一交易在实践中无限次执行。在这种情况下,防止在交易哈希计算过程中生成包含相同 batchId 的重复交易非常重要。

4. 抢跑

在 ERC4337 标准中,handleOps() 函数处理以数组捆绑在一起的多个 UserOperations。在此过程中,如果一个 UserOperation 失败,整个交易将被回滚。假设一个 Bundler 提交一个交易,并且攻击者通过抢跑先执行 UserOperation 数组中的最后一个 UserOperation。被攻击的 Bundler 验证了 UserOperations,直到最后一个 UserOperation,消耗了它们的Gas,然后在最后一个 UserOperation 失败,使整个 UserOperations 集被回滚。因此,受害者为用户操作支付了大部分Gas费用但却没有得到补偿,而且由于交易失败而无法收到付款。

为了防止这种情况,Bundlers 应采取措施以对抗抢跑。因此,Bundlers 应该是可以作为第一笔交易放在区块中的区块矿工,或者利用像 MEV-boost 这样的基础设施。此外,Bundlers 应该在每个区块中检查他们的 UserOperations 的有效性。第二层区块链,其中序列者是唯一可以创建区块的实体,应支持为 Bundlers 提供 RPC 端点,如以前的 文章 所述。

在 Ambire 中发现了一个问题,Relayer 可以通过抢跑强制回退通过特定函数执行的交易。AmbireAccount 合约包含一个名为 tryCatch() 的函数,该函数通过低级调用执行 UserOperation。如下面代码所示,tryCatch() 函数的实现方式在调用失败时不会回退。因此,即使 UserOperation 执行失败,Relayer 也可以得到交易提交的奖励。

// https://github.com/AmbireTech/wallet/blob/1ef5b7208e906be4287673746f15418984e78bc0/contracts/AmbireAccount.sol#L81-L87
function tryCatch(address to, uint256 value, bytes calldata data) external payable {
  require(msg.sender == address(this), 'ONLY_IDENTITY_CAN_CALL');
  (bool success, bytes memory returnData) = to.call{ value: value, gas: gasleft() }(data);
  if (!success) emit LogErr(to, value, data, returnData);
}

问题在于 Relayer 可以故意使 UserOperation 失败,并通过操控交易的Gas限制获得奖励。以太坊通过 EIP150 硬分叉引入了Gas费用政策的变化,其中引入了“1/64 规则”。这意味着 1/64 的Gas保留给调用上下文本身,其他的(gasLeft — gasLeft * 1/64)被传递给被调用的函数。换句话说,即使被调用的函数(子函数)因内部操作耗尽了Gas,对调用者(父函数)的执行还有一些Gas可以完成,因此不会使交易回退。

恶意的 Relayers 或者能够抢跑交易的实体可以利用这一行为来破坏账户的操作。当用户账户打算通过 tryCatch() 函数提交交易时,调用将按照以下顺序发生:

  1. 调用用户账户的 execute() 函数。

  2. 调用 tryCatch() 函数。

  3. 调用目标合约中的函数。

攻击者可以在提交的交易中精确设置Gas限制,导致目标合约的函数因Gas耗尽而失败,同时在 tryCatch() 的后续操作成功。换句话说,交易提交发生,但账户的预期行为不会被执行。

为了解决这个问题,Ambire 已修改合约以在 tryCatch() 函数中检查低级调用之前和之后的剩余Gas,并在调用后剩余Gas不足时回退交易。与 Ambire 的实现相比,Infinitism 的 ERC4337 参考实现 在调用 UserOperation 失败时回退 交易,原因是Gas不足错误。

5. ERC4337 合规性

在 Biconomy 中,存在多个部分不符合 ERC4337 标准的情况。出现了指定在 ERC4337 中的逻辑 未按正确顺序实现createSenderIfNeeded())、实施错误validateUserOp()),或 没有实施 (聚合器验证)的情况。

此外,实施中的修饰符功能可能会阻止标准的预期行为。例如,Biconomy 的 SmartAccount 合约中的 execute()executeBatch() 函数有一个 onlyOwner 修饰符,限制了对 EntryPoint 的交互。

在这种情况下,EntryPoint 合约无法调用这些函数,使用户账户无法与 EntryPoint 合约互动。基于 ERC4337 的账户抽象应该允许基于用户账户指定的 UserOperations 执行交易。这类特性应当移除,因为它们与原设计意图相悖,并限制了交易行为的范围。

Zerodev Kernel 也由于不符合 ERC4337 标准而存在漏洞。根据标准,Zerodev Kernel 的 ECDSAKernelFactory 合约应在 EntryPoint 质押一定数量的 ETH 才能访问存储。如果没有资金在 EntryPoint 质押,Bundlers 将不会执行利用该工厂的 UserOperations 中的交易。在 Zerodev Kernel 的案例中,发现 Bundlers 在他们应该访问的 EntryPoint 没有进行质押。

未能遵守该标准可能会导致未来交互时出现意外错误或与其他协议不兼容。由于不同实体可以操作账户抽象的组件,如 Bundler、Paymaster、EntryPoint 和 Wallet Factory,项目在实施过程中必须仔细验证与 ERC4337 标准的合规性。

此外,Zerodev Kernel 中的问题可能会导致在与外部库和标准交互时出现意外行为。在 Zerodev Kernel 内的 SessionKeyValidator 合约中,validateUserOp() 函数负责使用 Merkle Proof 机制验证 UserOperations,该机制是用来验证外部验证库。此函数返回 MerkleProofLib.verify() 的结果,而该库实现返回 1 代表正确,返回 0 代表错误。换句话说,它对于有效的 UserOperation 返回 1,对于无效的返回 0。

然而,在 EntryPoint 中,值 1 被解读为 SIG_VALIDATION_FAILED,表示签名验证失败。因此,使用 Merkle Proof 验证的 UserOperation 验证失败却在 EntryPoint 被识别为 UserOperation 的签名验证成功,可能导致用户资金盗窃。在项目实施过程中使用不同的验证库进行 UserOperation 验证时,必须彻底理解潜在情况,而非直接使用结果值。

变种逻辑中的漏洞

ERC4337 支持一种称为“反事实地址”的特性,允许用户在创建之前知道账户的地址,并允许向该地址转账。Biconomy 也支持该 反事实地址特性。然而,在 Biconomy 中存在一个问题,允许调用者在 创建反事实地址时指向任意 EntryPoint(与反事实钱包为同一地址)。在这一过程中,EntryPoint 的地址未包含在账户的创建中,只有 ownerindex 被用作盐。因此,攻击者可以在用户之前发现反事实钱包的地址,并在用户之前设置恶意的 EntryPoint。如果用户在创建之前提前给账户注入资金,攻击者可以将恶意合约设置为 EntryPoint并抽取资金。

在 ERC4337 标准中,用户不需要单独指定 EntryPoint。相反,账户实现(SimpleAccount.accountImplementation)仅利用预先注册的 EntryPoint 地址。因此,即使攻击者部署了一个账户,他们指定的 EntryPoint 也不会被使用,而是将使用预先注册的 EntryPoint 地址。

6. 实施保护

在 Biconomy 中,发生了一些问题,允许攻击者妨碍在 SmartAccount 工厂中指定的账户实现的所有权。如果在账户部署时未初始化实现,攻击者可能通过 selfdestruct() 摧毁实现,冻结指向某个特定实现的所有账户的操作。类似于这种情况,Ambire 的 AmbireAccount 合约也缺乏对其实现的保护,授权实体可以通过 constructor() 或服务本身摧毁实现。这两个案例导致指向实现账户的资金被冻结。

在 Infinitism 的 ERC4337 参考实现中,存在一个 漏洞 任何人都可以将管理器合约的模块配置为新。) 其中由于在 EIP4337Manager 合约中调用 selfdestruct()delegatecall,使得实现可能被销毁。此合约继承了来自 GnosisSafe 合约的功能,问题出在任何人都可以添加任意模块,如下面的 代码 所示。攻击者可能添加一个调用执行 selfdestruct() 的模块,从而销毁 EIP4337Manager 合约。

// https://github.com/eth-infinitism/account-abstraction/blob/6dea6d8752f64914dd95d932f673ba0f9ff8e144/contracts/gnosis/EIP4337Manager.sol#L66-L72
function setup4337Modules(
  EIP4337Manager manager //管理者(本合约)
) external {
  GnosisSafe safe = GnosisSafe(payable(this));
  safe.enableModule(manager.entryPoint());
  safe.enableModule(manager.eip4337Fallback());
}

7. 意外交易失败

Biconomy 提供了各种方法从用户账户执行交易,包括通过 EntryPoint 调用 execFromEntryPoint()execute() 函数,以及执行 execTransaction() 函数。Biconomy 的 SmartAccount 合约为 EntryPoints 维护了一个固定的 batchId 0。在 EntryPoint 执行账户的 validateUserOp() 时,nonces[0] 增加1。

batchId 0 应仅由 EntryPoints 调用。然而,在 execTransaction() 函数中没有对禁止其他 Relayers 使用 batchId 0 的限制。如果 execTransaction() 被调用,batchId 被设置为 0,则 batchId 0 的 nonce 将增加,随后的交易将由于更新过的 nonce 被回退。这种意外的交易失败可能导致 Relayers 财务损失并减少他们的激励活动。

前一篇文章中 所提,Infinitism 的实施也存在导致交易失败的漏洞。在 Infinitism 的标准实现中,EntryPoint 设计为在 UserOperations 执行阶段收到回退原因,并以 memory bytes 形式在交易回退时传递。然而,这一过程消耗了大量Gas来复制重入原因,尤其是在 EntryPoint 中长度极长的情况下,这可能导致强制交易失败。这种攻击可能发生在 UserOperation 执行阶段,在模拟阶段难以预测交易失败。类似地,EntryPoint 中的意外交易失败会导致 Bundlers 经济损失。因此,在设计独立的 EntryPoint 合约时,必须注意这一问题并加以考虑。

结论

在整篇文章中,我们讨论了基于 ERC4337 的账户抽象项目必须认识到的安全考虑。目前,基于 ERC4337 的实施仍处于早期阶段,除了文章中提到的项目,还有许多有前景的项目如 Gnosis Safe 正在积极开展 ERC4337 相关项目。由于许多这些项目尚未完成安全审计,因此存在因这些项目独立开发的特性而导致新漏洞出现的可能性。

考虑到迄今为止开发的项目初始实施中出现的各种问题,所有即将开展的项目在开发过程中需仔细考虑本文中提到的要点。

参考

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

0 条评论

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