智能合约开发账户抽象(ERC-4337)
高级4-5周
账户抽象(ERC-4337)
掌握 ERC-4337 账户抽象标准,实现智能合约钱包和无 Gas 交易
🎯 学习要点
理解 EOA 与智能合约账户的区别
掌握 UserOperation 生命周期
实现自定义验证逻辑(多签、生物识别)
设计 Paymaster 赞助 Gas 策略
实现社交恢复和会话密钥机制
📚 课程章节
ERC-4337 架构与核心概念3小时
UserOperation 与 Bundler4小时
Account 合约实现5小时
Paymaster 与 Gas 赞助4小时
Session Keys 与社交恢复5小时
代码示例
ERC-4337 核心接口
ERC-4337 的核心接口定义,Account 和 Paymaster 是关键组件
solidity
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.20;3 4/**5 * @title IAccount6 * @dev ERC-4337 账户接口7 */8interface IAccount {9 /**10 * @dev 验证 UserOperation 的签名11 * @param userOp UserOperation 结构12 * @param userOpHash UserOperation 哈希13 * @param missingAccountFunds 账户需要预付的资金14 * @return validationData 验证结果(0 表示成功)15 */16 function validateUserOp(17 UserOperation calldata userOp,18 bytes32 userOpHash,19 uint256 missingAccountFunds20 ) external returns (uint256 validationData);21}22 23/**24 * @dev UserOperation 结构体25 */26struct UserOperation {27 address sender; // 发送者(账户合约地址)28 uint256 nonce; // 防重放攻击29 bytes initCode; // 账户初始化代码(如果账户不存在)30 bytes callData; // 要执行的调用数据31 uint256 callGasLimit; // 执行调用的 Gas 限制32 uint256 verificationGasLimit; // 验证的 Gas 限制33 uint256 preVerificationGas; // 预验证 Gas34 uint256 maxFeePerGas; // 最大 Gas 价格35 uint256 maxPriorityFeePerGas; // 优先费36 bytes paymasterAndData; // Paymaster 地址和数据37 bytes signature; // 用户签名38}39 40/**41 * @title IPaymaster42 * @dev Paymaster 接口(赞助 Gas)43 */44interface IPaymaster {45 /**46 * @dev 验证 Paymaster 是否愿意为此操作付费47 */48 function validatePaymasterUserOp(49 UserOperation calldata userOp,50 bytes32 userOpHash,51 uint256 maxCost52 ) external returns (bytes memory context, uint256 validationData);53 54 /**55 * @dev 操作执行后的回调56 */57 function postOp(58 PostOpMode mode,59 bytes calldata context,60 uint256 actualGasCost61 ) external;62}63 64enum PostOpMode {65 opSucceeded,66 opReverted,67 postOpReverted68}简单账户合约实现
实现了基础的智能合约账户,支持签名验证和批量操作
solidity
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.20;3 4import "@account-abstraction/contracts/core/BaseAccount.sol";5import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";6import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";7 8/**9 * @title SimpleAccount10 * @dev 基础的 ERC-4337 智能合约账户11 */12contract SimpleAccount is BaseAccount {13 using ECDSA for bytes32;14 15 IEntryPoint private immutable _entryPoint;16 address public owner;17 18 event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);19 20 modifier onlyOwner() {21 _requireFromOwner();22 _;23 }24 25 constructor(IEntryPoint anEntryPoint) {26 _entryPoint = anEntryPoint;27 _disableInitializers();28 }29 30 /**31 * @dev 初始化账户(仅一次)32 */33 function initialize(address anOwner) public virtual initializer {34 _initialize(anOwner);35 }36 37 function _initialize(address anOwner) internal virtual {38 owner = anOwner;39 emit SimpleAccountInitialized(_entryPoint, owner);40 }41 42 /**43 * @dev 返回 EntryPoint44 */45 function entryPoint() public view virtual override returns (IEntryPoint) {46 return _entryPoint;47 }48 49 /**50 * @dev 验证 UserOperation 签名51 */52 function _validateSignature(53 UserOperation calldata userOp,54 bytes32 userOpHash55 ) internal virtual override returns (uint256 validationData) {56 bytes32 hash = userOpHash.toEthSignedMessageHash();57 58 if (owner != hash.recover(userOp.signature)) {59 return SIG_VALIDATION_FAILED;60 }61 return 0;62 }63 64 /**65 * @dev 执行操作(仅 EntryPoint 可调用)66 */67 function execute(68 address dest,69 uint256 value,70 bytes calldata func71 ) external {72 _requireFromEntryPointOrOwner();73 _call(dest, value, func);74 }75 76 /**77 * @dev 批量执行78 */79 function executeBatch(80 address[] calldata dest,81 uint256[] calldata value,82 bytes[] calldata func83 ) external {84 _requireFromEntryPointOrOwner();85 require(dest.length == func.length && value.length == func.length, "Length mismatch");86 87 for (uint256 i = 0; i < dest.length; i++) {88 _call(dest[i], value[i], func[i]);89 }90 }91 92 /**93 * @dev 内部调用94 */95 function _call(address target, uint256 value, bytes memory data) internal {96 (bool success, bytes memory result) = target.call{value: value}(data);97 if (!success) {98 assembly {99 revert(add(result, 32), mload(result))100 }101 }102 }103 104 /**105 * @dev 存款到 EntryPoint106 */107 function addDeposit() public payable {108 entryPoint().depositTo{value: msg.value}(address(this));109 }110 111 /**112 * @dev 提取 EntryPoint 存款113 */114 function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner {115 entryPoint().withdrawTo(withdrawAddress, amount);116 }117 118 function _requireFromOwner() internal view {119 require(msg.sender == owner, "Only owner");120 }121 122 function _requireFromEntryPointOrOwner() internal view {123 require(124 msg.sender == address(entryPoint()) || msg.sender == owner,125 "Only EntryPoint or owner"126 );127 }128 129 receive() external payable {}130}Paymaster 实现(Gas 赞助)
Paymaster 可以代付 Gas 费用,支持白名单、额度管理等策略
solidity
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.20;3 4import "@account-abstraction/contracts/core/BasePaymaster.sol";5 6/**7 * @title VerifyingPaymaster8 * @dev 基于签名验证的 Paymaster,支持白名单和 Gas 赞助9 */10contract VerifyingPaymaster is BasePaymaster {11 using ECDSA for bytes32;12 using UserOperationLib for UserOperation;13 14 address public verifyingSigner;15 16 // 每个用户的 Gas 额度17 mapping(address => uint256) public gasAllowance;18 19 constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) {20 verifyingSigner = _verifyingSigner;21 }22 23 /**24 * @dev 为用户充值 Gas 额度25 */26 function addGasAllowance(address user, uint256 amount) external onlyOwner {27 gasAllowance[user] += amount;28 }29 30 /**31 * @dev 验证是否赞助此操作32 */33 function _validatePaymasterUserOp(34 UserOperation calldata userOp,35 bytes32 userOpHash,36 uint256 maxCost37 ) internal override returns (bytes memory context, uint256 validationData) {38 // 解析 paymasterAndData39 (uint48 validUntil, uint48 validAfter, bytes memory signature) = 40 parsePaymasterAndData(userOp.paymasterAndData);41 42 // 验证 Paymaster 签名43 bytes32 hash = keccak256(44 abi.encode(45 userOp.sender,46 userOp.nonce,47 keccak256(userOp.initCode),48 keccak256(userOp.callData),49 userOp.callGasLimit,50 userOp.verificationGasLimit,51 userOp.preVerificationGas,52 userOp.maxFeePerGas,53 userOp.maxPriorityFeePerGas,54 block.chainid,55 address(this),56 validUntil,57 validAfter58 )59 ).toEthSignedMessageHash();60 61 require(verifyingSigner == hash.recover(signature), "Invalid signature");62 63 // 检查 Gas 额度64 require(gasAllowance[userOp.sender] >= maxCost, "Insufficient gas allowance");65 66 // 返回上下文(用于 postOp)67 context = abi.encode(userOp.sender, maxCost);68 validationData = _packValidationData(false, validUntil, validAfter);69 }70 71 /**72 * @dev 操作完成后扣除 Gas 额度73 */74 function _postOp(75 PostOpMode mode,76 bytes calldata context,77 uint256 actualGasCost78 ) internal override {79 (address sender, uint256 maxCost) = abi.decode(context, (address, uint256));80 81 if (mode == PostOpMode.opSucceeded || mode == PostOpMode.opReverted) {82 // 扣除实际消耗的 Gas83 gasAllowance[sender] -= actualGasCost;84 }85 }86 87 function parsePaymasterAndData(bytes calldata paymasterAndData)88 public89 pure90 returns (uint48 validUntil, uint48 validAfter, bytes memory signature)91 {92 validUntil = uint48(bytes6(paymasterAndData[20:26]));93 validAfter = uint48(bytes6(paymasterAndData[26:32]));94 signature = paymasterAndData[32:];95 }96}多签账户实现
多签账户允许多个签名者共同控制账户,提高安全性
solidity
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.20;3 4import "./SimpleAccount.sol";5 6/**7 * @title MultiSigAccount8 * @dev 支持多签验证的智能合约账户9 */10contract MultiSigAccount is SimpleAccount {11 address[] public owners;12 uint256 public threshold;13 14 mapping(address => bool) public isOwner;15 16 event OwnerAdded(address indexed owner);17 event OwnerRemoved(address indexed owner);18 event ThresholdChanged(uint256 threshold);19 20 constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) {}21 22 /**23 * @dev 初始化多签账户24 */25 function initializeMultiSig(26 address[] memory _owners,27 uint256 _threshold28 ) public initializer {29 require(_owners.length >= _threshold, "Invalid threshold");30 require(_threshold > 0, "Threshold must be > 0");31 32 for (uint256 i = 0; i < _owners.length; i++) {33 address owner = _owners[i];34 require(owner != address(0), "Invalid owner");35 require(!isOwner[owner], "Duplicate owner");36 37 isOwner[owner] = true;38 owners.push(owner);39 }40 41 threshold = _threshold;42 owner = address(this); // 设置为合约自己43 }44 45 /**46 * @dev 验证多签签名47 */48 function _validateSignature(49 UserOperation calldata userOp,50 bytes32 userOpHash51 ) internal view override returns (uint256 validationData) {52 bytes32 hash = userOpHash.toEthSignedMessageHash();53 bytes memory signatures = userOp.signature;54 55 require(signatures.length >= threshold * 65, "Insufficient signatures");56 57 address lastSigner = address(0);58 uint256 validSignatures = 0;59 60 for (uint256 i = 0; i < threshold; i++) {61 bytes memory signature = new bytes(65);62 for (uint256 j = 0; j < 65; j++) {63 signature[j] = signatures[i * 65 + j];64 }65 66 address signer = hash.recover(signature);67 68 // 签名者必须是 owner 且按地址升序排列(防止重复)69 require(signer > lastSigner, "Invalid signer order");70 require(isOwner[signer], "Not an owner");71 72 lastSigner = signer;73 validSignatures++;74 }75 76 if (validSignatures >= threshold) {77 return 0;78 }79 80 return SIG_VALIDATION_FAILED;81 }82 83 /**84 * @dev 添加 owner(需要多签确认)85 */86 function addOwner(address newOwner, uint256 newThreshold) external {87 _requireFromEntryPointOrOwner();88 require(newOwner != address(0), "Invalid owner");89 require(!isOwner[newOwner], "Already an owner");90 require(newThreshold <= owners.length + 1, "Invalid threshold");91 92 isOwner[newOwner] = true;93 owners.push(newOwner);94 95 if (newThreshold > 0) {96 threshold = newThreshold;97 emit ThresholdChanged(newThreshold);98 }99 100 emit OwnerAdded(newOwner);101 }102 103 /**104 * @dev 移除 owner105 */106 function removeOwner(address ownerToRemove, uint256 newThreshold) external {107 _requireFromEntryPointOrOwner();108 require(isOwner[ownerToRemove], "Not an owner");109 require(owners.length - 1 >= newThreshold, "Invalid threshold");110 111 isOwner[ownerToRemove] = false;112 113 // 从数组中移除114 for (uint256 i = 0; i < owners.length; i++) {115 if (owners[i] == ownerToRemove) {116 owners[i] = owners[owners.length - 1];117 owners.pop();118 break;119 }120 }121 122 threshold = newThreshold;123 emit OwnerRemoved(ownerToRemove);124 emit ThresholdChanged(newThreshold);125 }126}