拓冰建站拓冰建站
首页 / 资讯中心 / 正文

如何用 OpenZeppelin Contracts 编写 ERC-4337 智能账户合约并通过 Factory 部署账户?

如何用 OpenZeppelin Contracts 编写 ERC-4337 智能账户合约并通过 Factory 部署账户【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts本文面向要基于 OpenZeppelin Contracts 开发 ERC-4337 智能账户的开发者。你要完成的任务是编写一个Account合约定义签名验证逻辑编写一个基于Clones的 Factory确定性创建账户实例并把二者串起来——首次发送UserOperation时通过initCode字段在同一笔用户操作里部署账户再通过handleOps完成提交与验证。资料来源是仓库内的 accounts.adoc、account-abstraction.adoc 以及 contracts/account/ 下的源码。先明确各组件的角色ERC-4337 通过替代 mempool 处理用户操作核心组件在 account-abstraction.adoc 中有定义UserOperation伪交易对象v0.8 起为PackedUserOperation结构struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; // concatenation of factory address and factoryData (or empty) bytes callData; bytes32 accountGasLimits; // verificationGasLimit (16 bytes) callGasLimit (16 bytes) uint256 preVerificationGas; bytes32 gasFees; // maxPriorityFeePerGas (16 bytes) maxFeePerGas (16 bytes) bytes paymasterAndData; bytes signature; }EntryPointUserOperation的执行入口是跨网络部署在同一地址的 singleton 合约。ERC4337Utils.sol 中记录了 v0.7/v0.8/v0.9 的地址其中ENTRYPOINT_V08 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108ENTRYPOINT_V09 0x433709009B8330FDa32311DF1C2AFA402eD8D009。注意Account.sol 的entryPoint()默认返回ENTRYPOINT_V09而文档里的签名示例 domain 用的是 v0.8 地址——两处版本不一致是文档现状你的 EIP-712 domain 必须与账户entryPoint()实际指向的 EntryPoint 一致使用前请先对齐版本。Account Contract实现IAccount的validateUserOp(PackedUserOperation calldata, bytes32, uint256)来验证操作执行侧可以走fallback或实现可选的IAccountExecute接口文档也提到可以用 ERC-7821 作为最小批量执行接口。Factory Contract由账户开发者定义接收任意字节initData返回账户逻辑部署到的address。账户地址是确定性的——代码与地址可预测这正是initCode机制的前提。编写账户合约继承 Account 并选择 Signeraccounts.adoc 指出Account的最小要求是提供 _rawSignatureValidation 的实现。库提供了现成的Signer特化合约可复用SignerECDSAEOA 签名、SignerP256secp256r1适用于 FIDO/passkey、SignerRSA、SignerEIP7702、SignerERC7913及多签器MultiSignerERC7913/MultiSignerERC7913Weighted等。若你使用自定义签名方案文档建议优先考虑自带 ERC-7913 verifier 而非自行实现AbstractSigner。以 ECDSA 为例文档给出的账户模板initializable 设计方便 Factory 部署后立即初始化import {Account} from openzeppelin/community-contracts/account/Account.sol; import {Initializable} from openzeppelin/contracts/proxy/utils/Initializable.sol; import {SignerECDSA} from openzeppelin/contracts/utils/cryptography/signers/SignerECDSA.sol; contract MyAccount is Initializable, Account, SignerECDSA, ... { // ... function initializeECDSA(address signer) public initializer { _setSigner(signer); } }两个必须注意的限制_setSigner必须调用。SignerECDSA.sol 的注释明确警告如果在构造独立部署时或初始化clone 时期间未调用_setSignersigner 可能处于可被抢跑或不可用状态。同时 accounts.adoc 也有 WARNING账户保持未初始化状态会使其不可用因为没有公钥与之关联。核心Account不包含任意外部调用机制。Account.sol 的注释说明这是每个账户都应具备的能力留给你自行实现常见选择包括 ERC-7579、ERC-7821 等。另外账户不原生支持 ERC-721 / ERC-1155 代币接收方需要接受检查文档建议继承ERC721Holder/ERC1155Holder。编写 Factory用 Clones 做确定性部署文档推荐用 Clones 库 自建工厂利用最小化克隆降低成本并利用地址可预测性。示例合约在 MyFactoryAccount.solaccounts.adoc中引用的就是这份代码。下面是同一份代码import 路径按文档中初始化示例的 npm 包风格改写若在项目内使用保持原仓库相对路径即可import {Clones} from openzeppelin/contracts/proxy/Clones.sol; import {Address} from openzeppelin/contracts/utils/Address.sol; contract MyFactoryAccount { using Clones for address; using Address for address; address private immutable _impl; constructor(address impl_) { require(impl_.code.length 0); _impl impl_; } /// dev Predict the address of the account function predictAddress(bytes calldata callData) public view returns (address) { return _impl.predictDeterministicAddress(keccak256(callData), address(this)); } /// dev Create clone accounts on demand function cloneAndInitialize(bytes calldata callData) public returns (address) { address predicted predictAddress(callData); if (predicted.code.length 0) { _impl.cloneDeterministic(keccak256(callData)); predicted.functionCall(callData); } return predicted; } }工作方式cloneAndInitialize(callData)先用keccak256(callData)作为 salt 预测地址若该地址还没有代码就执行确定性克隆并立即functionCall(callData)——这就是文档说的Factory 部署后立即在同一笔交易中初始化账户。callData通常就是initializeECDSA(signer)的编码数据。文档特别强调工厂必须确保账户地址与初始 owners 确定性绑定防止恶意者抢跑部署账户。具体做法是把 owner 地址纳入地址计算的 salt上述示例中 owner 包含在callData里因此已体现在 salt 中。部署顺序先实现、后工厂文档没有给出具体部署命令但工厂构造器constructor(address impl_)与initializeECDSA的初始化流程决定了顺序部署MyAccount实现合约注意实现合约本身不做初始化_setSigner留给工厂在 clone 后调用。部署MyFactoryAccount构造参数传入MyAccount实现地址构造器会require(impl_.code.length 0)所以实现必须先存在。记录工厂地址——它后面会出现在UserOperation的initCode里。构建第一个 UserOperation 并触发部署用 viem 准备UserOperationsender、nonce、accountGasLimits、callData是必填项signature之后签名填入。代码中0x...形式的是你必须替换的值YOUR_ACCOUNT_ADDRESS是工厂预测出的账户地址ENTRYPOINT_ADDRESS是目标链的 EntryPoint 地址CALLDATA_TO_EXECUTE_IN_THE_ACCOUNT是账户要执行的调用数据如execute的编码数据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: 0xCALLDATA_TO_EXECUTE_IN_THE_ACCOUNT, 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)文档建议先检查预测地址是否已有代码未部署时才填入const deployed await publicClient.getCode({ address: predictedAddress }); if (!deployed) { userOp.initCode encodePacked( [address, bytes], [ 0xACCOUNT_FACTORY_ADDRESS, encodeFunctionData({ abi: [/* ACCOUNT ABI */], functionName: FUNCTION NAME, // 例如 initializeECDSA args: [signer], }), ] ); }文档示例中encodeFunctionData的functionName/args是占位写法对应你的工厂callData应为initializeECDSA及其 signer 参数。Gas 参数取值文档给出的估算指引这些是文档标注的典型值不是硬性上限随签名验证复杂度变化verificationGasLimit覆盖签名验证、paymaster 校验如有与账户验证逻辑文档称典型值约 100,000 gascallGasLimit覆盖账户实际执行建议对每个子调用用eth_estimateGas再加缓冲preVerificationGas补偿 EntryPoint 执行开销文档称 50,000 是合理起点视 UserOperation 大小调整maxFeePerGas/maxPriorityFeePerGas通常由 bundler 服务通过 SDK 或自定义 RPC 提供。另有一条必须知道的惩罚规则当callGasLimit或paymasterPostOpGasLimit的未用 gas 达到 40,000PENALTY_GAS_THRESHOLD以上时未用部分会被收取 10%UNUSED_GAS_PENALTY_PERCENT罚金。签名与提交签名分三步用 EntryPoint 的 domain 计算 EIP-712 typed data 哈希 → 用账户的签名方案签名 → 把签名按账户合约期望的格式编码进signature字段。文档示例基于 EntryPoint v0.8 domainimport { 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 的getUserOpHash拿到原始哈希后直接签名。文档 IMPORTANT 提示这种方式用户体验较差——用户看到的是不可读哈希且许多链下签名器不支持签原始哈希const userOpHash await entrypoint.read.getUserOpHash([userOp]); userOp.signature await eoa.sign({ hash: userOpHash });最后调用handleOps提交并以waitForTransactionReceipt拿到回执作为验证方式args第二项beneficiary是接收 gas 费的地址文档建议设为自己const userOpReceipt await walletClient .writeContract({ abi: [/* ENTRYPOINT ABI */], address: 0xENTRYPOINT_ADDRESS, functionName: handleOps, args: [[userOp], eoa.address], }) .then((txHash) publicClient.waitForTransactionReceipt({ hash: txHash, }) ); console.log(userOpReceipt);成功判据按文档给出的检查方式userOpReceipt返回回执说明交易已上链对首次部署场景回执后可再次对predictedAddress执行getCode确认账户代码已存在部署前!deployed分支正是用同一方法判断的。如果你是自打包自己调用handleOps文档 TIP 指出preVerificationGas和maxFeePerGas可以安全地填 0。若追求更高可靠性文档建议改用 bundler 服务它们自动处理 gas 估算、交易排序与多操作打包成功率更高。限制与后续安全项未初始化即不可用工厂部署 clone 后必须走initializeECDSA之类的 initializer否则账户没有关联公钥、无法使用accounts.adoc 的 WARNING。防跨账户重放文档 IMPORTANT 推荐配合 ERC7739 做防御性重哈希避免同一签名者在多个账户间的签名重放同时避免让用户签不可读的混淆哈希钓鱼风险。ERC-7562 验证规则账户在验证阶段会读取自身存储如公钥可能以间接方式违反 ERC-7562 的存储访问限制文档指出违反规则仍可能由私有 bundler 处理但存在中心化取舍开发时需注意。版本对齐再次强调签名示例 domain 用 v0.8 EntryPoint而 Account.sol 的entryPoint()默认指向 v0.9两者必须一致否则validateUserOp的调用方校验onlyEntryPoint会失败。完成上述流程后你得到的是一条可核对的链路实现合约 → 工厂 → 预测地址 →initCode触发部署 →handleOps回执且每一环都有对应的检查手段getCode、getUserOpHash、回执。若后续要给账户加批量执行可看accounts.adoc中 ERC-7821 小节要支持模块化管理则看 AccountERC7579。【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门