如何用 OpenZeppelin Contracts ERC-7821 为智能账户实现批量执行?

发布时间:2026/9/13 18:25:17
如何用 OpenZeppelin Contracts ERC-7821 为智能账户实现批量执行? 如何用 OpenZeppelin Contracts ERC-7821 为智能账户实现批量执行【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts如果你已经有一个基于 ERC-4337 的智能账户但每笔转账、每次approvetransfer都要单独发一个 UserOperation本文的任务就是给这个账户接上 OpenZeppelin Contracts 的ERC7821扩展让它在一次执行中原子地完成多个调用并讲清楚批次数据如何编码、如何提交、以及如何判断批次是否真的生效。适用前提你的账户是或将要继承本仓库 Account 的实现并且目标链支持 ERC-4337 EntryPoint如果你只是想让 EOA 直接批量执行、不依赖 ERC-4337 基础设施文末给出了 EIP-7702 委托这条可选路径。ERC-7821 实现提供什么边界在哪里ERC-7821 是一个最小批量执行器标准接口只有两个函数见 draft-IERC7821.solexecute(bytes32 mode, bytes executionData)执行executionData中编码的调用任一子调用失败则整体 revert 并冒泡错误supportsExecutionMode(bytes32 mode)供前端检测账户是否支持某个执行模式。本仓库的实现 ERC7821 是一个抽象合约必须被你的账户继承。它有四个明确的约束直接决定后面的编码方式只支持单一批次模式0x01000000000000000000bytes1 callType 0x01批量 bytes1 execType 0x00默认执行 全零 selector/payload不支持接口注释中提到的带opData的模式0x01000000000078210001...不携带可选opDataexecutionData就是abi.encode(calls)其中calls是(address,uint256,bytes)[]类型数组即每项为{target, value, data}默认访问控制只允许账户自己_erc7821AuthorizedExecutor默认返回caller address(this)未授权调用会 revert 出Account.AccountUnauthorized(caller)要通过 EntryPoint 执行必须覆盖该函数放行entryPoint()合约是stateless的无存储因此也可以作为不依赖 ERC-4337 代码的独立批量执行器使用。另外按 ERC-7579 执行约定批量中某一笔调用的target若为address(0)会被替换为address(this)。第一步在账户合约中接入 ERC-7821最小做法是让账户同时继承Account、你的签名器实现和ERC7821。文档给出的示例如下import {Account} from openzeppelin/community-contracts/account/Account.sol; import {ERC7821} from openzeppelin/community-contracts/account/extensions/draft-ERC7821.sol; import {SignerEIP7702} from openzeppelin/contracts/utils/cryptography/signers/SignerEIP7702.sol; contract MyAccount is Account, SignerEIP7702, ERC7821 { // Override to allow the entrypoint to execute batches function _erc7821AuthorizedExecutor( address caller, bytes32 mode, bytes calldata executionData ) internal view virtual override returns (bool) { return caller address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData); } }覆盖_erc7821AuthorizedExecutor是接入后必须做的一步不覆盖的话只有账户自己能调executeEntryPoint 转发的调用会被AccountUnauthorized拒掉。多签账户的文档示例multisig.adoc中MultiSignerERC7913、MultiSignerERC7913Weighted等实现都是同样的接法导入路径写作openzeppelin/contracts/account/extensions/draft-ERC7821.sol对应本仓库中的 contracts/account/extensions/draft-ERC7821.sol。第二步编码批次数据批次的调用数据由mode和batch两部分组成最终拼成execute(mode, batch)的 calldata。文档用 viem 演示了编码过程下面的target、data是占位值需替换为你的真实目标地址和函数调用数据// CALL_TYPE_BATCH, EXEC_TYPE_DEFAULT, ..., selector, payload const mode encodePacked( [bytes1, bytes1, bytes4, bytes4, bytes22], [0x01, 0x00, 0x00000000, 0x00000000, 0x00000000000000000000000000000000000000000000] ); const entries [ { target: 0x000...0001, // 替换为你的第一个目标地址 value: 0n, data: 0x000...000, // 替换为你的第一个调用数据 }, { target: 0x000...0002, // 替换为你的第二个目标地址 value: 0n, data: 0x000...000, // 替换为你的第二个调用数据 } ]; const batch encodeAbiParameters( [parseAbiParameter((address,uint256,bytes)[])], [ entries.map[Address, bigint, Hex]((entry) [entry.target, entry.value ?? 0n, entry.data ?? 0x] ), ] ); const userOpData encodeFunctionData({ abi: account.abi, functionName: execute, args: [mode, batch] });其中mode的五段拼装与supportsExecutionMode的判定一一对应0x01批量调用、0x00默认执行失败即 revert、两个bytes4的 selector 与bytes22的 payload 均为全零。userOpData就是账户callData字段要放的内容。第三步通过 ERC-4337 UserOperation 提交批次把userOpData装进一个 PackedUserOperation按以下顺序处理以下代码取自文档sender、EntryPoint 地址、工厂地址均为占位符需替换为你的真实值import { getContract, createWalletClient, http, Hex } from viem; const walletClient createWalletClient({ account, // See Viems privateKeyToAccount chain, // import { ... } from viem/chains; transport: http(), }) const entrypoint getContract({ abi: [/* ENTRYPOINT ABI */], address: 0xENTRYPOINT_ADDRESS, client: walletClient, }); const userOp { sender: 0xYOUR_ACCOUNT_ADDRESS, nonce: await entrypoint.read.getNonce([sender, 0n]), initCode: 0x as Hex, callData: userOpData, // 上一步编码出的 execute(mode, batch) calldata accountGasLimits: encodePacked( [uint128, uint128], [ 100_000n, // verificationGasLimit 300_000n, // callGasLimit ] ), preVerificationGas: 50_000n, gasFees: encodePacked( [uint128, uint128], [ 0n, // maxPriorityFeePerGas 0n, // maxFeePerGas ] ), paymasterAndData: 0x as Hex, signature: 0x as Hex, };账户尚未部署时initCode需要写成abi.encodePacked(factory, factoryData)以便在同一个 UserOp 中创建账户const deployed await publicClient.getCode({ address: predictedAddress }); if (!deployed) { userOp.initCode encodePacked( [address, bytes], [ 0xACCOUNT_FACTORY_ADDRESS, encodeFunctionData({ abi: [/* ACCOUNT ABI */], functionName: FUNCTION NAME, args: [...], }), ] ); }气体字段的取值依据文档给出的参考范围verificationGasLimit覆盖签名验证与账户验证逻辑典型值约 100,000随签名方案复杂度变化callGasLimit覆盖账户实际执行文档建议对每个子调用做eth_estimateGas后加缓冲preVerificationGas补偿 EntryPoint 开销50,000 是文档给出的起步值。注意一条与批量执行直接相关的成本规则当未使用的callGasLimit/paymasterPostOpGasLimit达到或超过 40,000PENALTY_GAS_THRESHOLD时未用部分会被收取 10%UNUSED_GAS_PENALTY_PERCENT罚金。签名用 EntryPoint 的 EIP-712 域对 PackedUserOperation 进行 typed-data 签名import { signTypedData } from viem/actions; // EntryPoint v0.8 EIP-712 domain const domain { name: ERC4337, version: 1, chainId: 1, // Your target chain ID verifyingContract: 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108, // v08 }; // EIP-712 types for PackedUserOperation const types { PackedUserOperation: [ { name: sender, type: address }, { name: nonce, type: uint256 }, { name: initCode, type: bytes }, { name: callData, type: bytes }, { name: accountGasLimits, type: bytes32 }, { name: preVerificationGas, type: uint256 }, { name: gasFees, type: bytes32 }, { name: paymasterAndData, type: bytes }, ], } as const; // Sign the UserOperation using EIP-712 userOp.signature await eoa.signTypedData({ domain, types, primaryType: PackedUserOperation, message: { sender: userOp.sender, nonce: userOp.nonce, initCode: userOp.initCode, callData: userOp.callData, accountGasLimits: userOp.accountGasLimits, preVerificationGas: userOp.preVerificationGas, gasFees: userOp.gasFees, paymasterAndData: userOp.paymasterAndData, }, });也可以用entrypoint.read.getUserOpHash([userOp])拿到原始哈希再签名但文档提醒原始哈希签名会给出体验较差的不透明消息且很多链下签名器不提供签原始哈希的能力。最后调用 EntryPoint 的handleOps提交// Send the UserOperation const userOpReceipt await walletClient .writeContract({ abi: [/* ENTRYPOINT ABI */], address: 0xENTRYPOINT_ADDRESS, functionName: handleOps, args: [[userOp], eoa.address], }) .then((txHash) publicClient.waitForTransactionReceipt({ hash: txHash, }) ); // Print receipt console.log(userOpReceipt);如果你自己打包 UserOperation而非交给 bundler文档提示可以安全地把preVerificationGas和maxFeePerGas设为 0。追求可靠性时文档建议改用 bundler 服务它们处理气体估算、排序与多操作打包。可选路径用 EIP-7702 委托直接执行不走 ERC-4337由于ERC7821是 stateless 的文档指出你可以把 EOA 委托delegate到一个ERC-7821实例上获得一个不使用 ERC-4337 相关代码的最小批量执行器。流程见 eoa-delegation.adoc先用 EOA 私钥签 EIP-7702 授权viem 的signAuthorizationcontractAddress指向你的批量执行器再发送一笔SET_CODE_TX_TYPE0x04交易把授权与批量 calldata 一起带上// Send the authorization along with data const receipt await walletClient .sendTransaction({ authorizationList: [authorization], data: 0xCALLDATA_TO_EXECUTE_IN_THE_ACCOUNT, // 即上一步的 execute(mode, batch) calldata to: eoa.address, }) .then((txHash) publicClient.waitForTransactionReceipt({ hash: txHash, }) );要解除委托发送指向零地址的授权即可清空代码但不会清理 EOA 存储文档对此有 WARNING。委托链路的验证方式与 ERC-4337 路径相同普通交易回执 目标合约事件。如何验证批次生效以及常见失败现象文档与仓库测试ERC7821.behavior.js给出了可对照的行为清单能力检测对账户调用supportsExecutionMode(0x01000000000000000000)支持时返回true其他模式包括带opData的0x01000000000078210001...返回false调用execute时则 revert 出UnsupportedExecutionMode()批次成功handleOps的callData为批次时测试验证了目标合约事件被触发如MockFunctionCalledExtra带正确参数、批量中向 EOA 转 ETH 的余额变化changeEtherBalances、以及账户 nonce 从 0 变为 1账户未部署时同一 UserOp 还会触发AccountDeployed事件未授权调用非 EntryPoint、非账户自身调用execute时revert 出AccountUnauthorized且参数为调用者地址——这是忘记覆盖_erc7821AuthorizedExecutor时的典型现象签名无效把signature置为0x00后handleOps整体 revert原子性实现按EXECTYPE_DEFAULT执行批次任一子调用失败则整笔执行 revert不存在部分成功状态。限制与注意本实现不支持ERC-7821 接口注释中带opData的模式因此无法在批次里携带签名、paymaster 数据或每笔子调用的 gas limit需要这些能力时只能自定义实现默认_erc7821AuthorizedExecutor只允许账户自身不覆盖就无法从 EntryPoint 执行批次Account类账户原生不支持接收 ERC-721 / ERC-1155接收地址需实现接收检查文档建议继承ERC721Holder/ERC1155Holder如果你的批次目标是转账这两类 token需要先在账户中加上这些 Holder 混入账户未初始化时可能因未关联公钥而不可用文档 WARNING工厂部署场景请在initCode里完成初始化。完成以上接入后你的账户就具备了一个可被 EntryPoint或 EOA 委托调用的原子批量入口按execute(mode, batch)编码、按文档的 gas 参考值填好 UserOperation、用 EIP-712 签名后经handleOps提交再用回执、目标事件和 nonce 变化确认批次整体生效。【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询