本文深入探讨了ERC-4337(账户抽象)及其在以太坊中的应用,提供了创建和部署ERC-4337符合的智能合约的具体步骤,包括使用Stackup SDK的实用指南。文章回顾了账户抽象的基本概念,细述了ERC-4337中的关键组件,如UserOperations和Bundlers,并提供了详细的代码示例和操作指导,帮助读者更好地理解和应用这一技术。
在我们的 Account Abstraction and ERC-4337 - Part 1 指南中,我们为理解 EIP-4337 奠定了基础。在本后续指南中,我们将进行实践,深入探讨使用 Stackup 创建和部署与 ERC-4337 兼容的智能合约的具体步骤。准备好深入探索了吗?让我们开始吧!
在我们深入研究 ERC-4337 的实现之前,让我们重温本指南系列的 第 1 部分 中提到的以太坊账户抽象的基本概念:
UserOperation 对象与我们在以太坊中看到的交易对象具有类似的字段。然而,nonce 和签名等字段是账户特定的(由 ERC-4337 实现)。EntryPoint 合约进行。由于 Bundlers 有激励措施保持活跃,它们会收取费用并优先选择哪些 UserOperation 进行打包以实现最大利润。UserOperation 对象。现在我们已经刷新了对 ERC-4337 概念的记忆,让我们深入探讨如何构建和与 ERC-4337 兼容的智能合约进行交互。
以太坊基金会实现了一个最小化的 ERC-4337 兼容合约示例,名为 SimpleAccount.sol。
让我们花几分钟时间查看以下代码。我们不需要创建此代码的文件,但可以仅查看以理解其功能。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */
/* solhint-disable reason-string */
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import "../core/BaseAccount.sol";
import "./callback/TokenCallbackHandler.sol";
/**
  * 最小账户。
  *  这是示例最小账户。
  *  具有执行、eth 处理方法
  *  具有一个签名者,可以通过 entryPoint 发送请求。
  */
contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
    using ECDSA for bytes32;
    address public owner;
    IEntryPoint private immutable _entryPoint;
    event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);
    modifier onlyOwner() {
        _onlyOwner();
        _;
    }
    /// @inheritdoc BaseAccount
    function entryPoint() public view virtual override returns (IEntryPoint) {
        return _entryPoint;
    }
    // solhint-disable-next-line no-empty-blocks
    receive() external payable {}
    constructor(IEntryPoint anEntryPoint) {
        _entryPoint = anEntryPoint;
        _disableInitializers();
    }
    function _onlyOwner() internal view {
        // 直接来自 EOA 拥有者,或通过账户本身(通过 execute() 重定向)
        require(msg.sender == owner || msg.sender == address(this), "only owner");
    }
    /**
     * 执行交易(由拥有者直接调用,或通过 entryPoint 调用)
     */
    function execute(address dest, uint256 value, bytes calldata func) external {
        _requireFromEntryPointOrOwner();
        _call(dest, value, func);
    }
    /**
     * 执行一系列交易
     * @dev 为了减少 trivial 状态下的 gas 消耗(没有值),使用零长度数组来表示零值
     */
    function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external {
        _requireFromEntryPointOrOwner();
        require(dest.length == func.length && (value.length == 0 || value.length == func.length), "wrong array lengths");
        if (value.length == 0) {
            for (uint256 i = 0; i < dest.length; i++) {
                _call(dest[i], 0, func[i]);
            }
        } else {
            for (uint256 i = 0; i < dest.length; i++) {
                _call(dest[i], value[i], func[i]);
            }
        }
    }
    /**
     * @dev _entryPoint 成员是不可变的,以降低 gas 消耗。要升级 EntryPoint,
     * 必须部署一个新的 SimpleAccount 实现并带上新的 EntryPoint 地址,然后通过调用 `upgradeTo()` 升级实现
     */
    function initialize(address anOwner) public virtual initializer {
        _initialize(anOwner);
    }
    function _initialize(address anOwner) internal virtual {
        owner = anOwner;
        emit SimpleAccountInitialized(_entryPoint, owner);
    }
    // 要求函数调用通过 EntryPoint 或所有者
    function _requireFromEntryPointOrOwner() internal view {
        require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint");
    }
    /// 实现 BaseAccount 的模板方法
    function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
    internal override virtual returns (uint256 validationData) {
        bytes32 hash = userOpHash.toEthSignedMessageHash();
        if (owner != hash.recover(userOp.signature))
            return SIG_VALIDATION_FAILED;
        return 0;
    }
    function _call(address target, uint256 value, bytes memory data) internal {
        (bool success, bytes memory result) = target.call{value : value}(data);
        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
    }
    /**
     * 检查当前账户在 entryPoint 的存款
     */
    function getDeposit() public view returns (uint256) {
        return entryPoint().balanceOf(address(this));
    }
    /**
     * 在 entryPoint 中为此账户存入更多资金
     */
    function addDeposit() public payable {
        entryPoint().depositTo{value : msg.value}(address(this));
    }
    /**
     * 从账户的存款中提取资金
     * @param withdrawAddress 目标地址
     * @param amount 要提取的金额
     */
    function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
        entryPoint().withdrawTo(withdrawAddress, amount);
    }
    function _authorizeUpgrade(address newImplementation) internal view override {
        (newImplementation);
        _onlyOwner();
    }
}让我们回顾一下代码。
以上 SimpleAccount 合约由一个外部拥有者地址控制,旨在与符合 ERC-4337 标准的 EntryPoint 合约进行交互,允许拥有者无需自己支付 gas 即可执行交易。该合约集成了 OpenZeppelin 的库,提供其他功能,例如加密签名验证(ECDSA)和可升级合约模式(UUPSUpgradeable 和 Initializable)。它还导入了 BaseAccount 和回调处理程序。BaseAccount 是一个核心组件,跟踪智能合约的 nonce,帮助验证 UserOperation 批量有效负载,进行 EntryPoint 交互,支付执行费用(即 payPrefund()),并具备扩展性,允许自定义实现函数,例如 _validateSignature()、_validateNonce() 和 _payPrefund()。
状态变量 owner 存储账户的拥有者地址,_entryPoint 是对外部合约的不可改变的引用,作为 EntryPoint。
两个主要功能 execute 和 executeBatch 允许 owner 或中继系统的入口点发送交易或一系列交易。两个功能会首先检查发送者是否为 EntryPoint 或拥有者,然后进行处理。
该合约还支持将拥有者升级到新拥有者,但任何 EntryPoint 的更新(例如,_entryPoint)都需要部署一个新的智能合约账户。
现在,为了简化处理,我们将使用 Stackup 的 SDK 部署一个 ERC-4337 兼容的合约,并开始执行诸如批准 ERC-20 代币和转移以太币及代币等操作。
我们将在本节中开发的 ERC-4337 兼容合约来自 Stackup。这是一个非常适合刚开始进行账户抽象开发的开发者的入门模板。
1. 首先,打开你的终端窗口并运行以下终端命令:
git clone https://github.com/stackup-wallet/erc-4337-examples.git
cd erc-4337-examples
yarn install上述命令克隆并安装相关 GitHub 存储库的依赖项。
2. 接下来,我们将使用 init 命令配置我们的 ERC-4337 合约:
yarn run init这将创建一个 config.json 文件,包含以下值:
rpcUrl:此 RPC URL 将支持我们从 ERC-4337 合约调用的方法。该字段需要 Stackup 的 API KeysigningKey:用于生成 UserOperation 签名的密钥。它也被合约账户用于验证交易paymaster.rpcUrl:用于生成 UserOperation 签名的密钥。该签名被合约账户用于验证交易paymaster.context:与你正在交互的 paymaster 相关的任意字段3. 现在,生成的 config.json 中,我们需要填写 RPC URL 等值。为此,请访问 https://app.stackup.sh/sign-in,创建一个帐户,然后将提示你选择一个链。选择 Etheruem Sepolia 链(为了本指南的目的),然后点击下一步。接着,点击你创建的 bundler 实例,并点击 API Key 按钮。获取到 API Key 后,返回到你的 config.json 文件中,并将 API Key 输入到所有 rpcUrl 字段中。
你的 config.json 应类似如下所示:
{
  "rpcUrl": "https://api.stackup.sh/v1/node/cd9af3b13c47d203af0b48513615bef69ec8c9072c24bbf2fd9ed9c8f97d6428",
  "signingKey": "0xbeacd206e9870af02243e2c1cd253a1440966f04c553d7e696c0271a17edd9e",
  "paymaster": {
    "rpcUrl": "https://api.stackup.sh/v1/paymaster/cd9af3b13c47d203af0b48513615bef69ec8c9072c24bbf2fd9ed9c8f97d6428",
    "context": {}
  }
}请记得保存文件!
4. 通过配置设置,我们可以创建一个如配置文件所定义的智能合约账户。请在你的终端中运行以下命令,将返回一个地址。智能合约账户尚未部署,但地址将生成,以便我们事先知道。
yarn run simpleAccount address要查看实际执行的内容,请导航到
scripts/simpleAccount/address.ts文件。
你应该看到类似的输出:
$ ts-node scripts/simpleAccount/index.ts address
SimpleAccount address: 0xD4494616f04ebd65E407330672c4C5A07BA5270F
✨  Done in 1.75s.在下一部分,我们将为刚生成的 SimpleAccount 地址提供资金。请注意,合约尚未部署。
现在,让我们为在上一部分中生成的智能合约账户(例如 SimpleAccount)地址提供资金。
你可以使用 QuickNode Multi-Chain Faucet 向你的个人钱包发送一些测试网 ETH,然后再转至 SimpleAccount 地址。请注意,Faucet 要求你在待资助地址上拥有主网余额。如果你已经在另一个钱包中拥有测试版 ETH,你也可以将其转移到你的智能合约(SimpleAccount)地址,而不是先使用 Faucet。

还可以找到一个备用的 Sepolia Faucet 这里。使用时请小心。
在为我们的智能合约账户(例如 SimpleAccount)提供资金后,我们现在可以从我们的智能合约账户发起转账。我们建议至少有 0.01 ETH 以测试 ETH 转账(加上 gas 费用)。在终端窗口中粘贴以下命令,但请记住用实际值替换占位符,例如 address 和 eth。
yarn run simpleAccount transfer --to {address} --amount {eth}若要查看运行此命令时执行的代码,请导航到
scripts/simpleAccount/transfer.ts文件。
简单来说,上述命令部署 SimpleAccount 合约,创建一个 UserOperation 有效载荷,签名后将其发送给 Bundler(如 config.json 中定义)。
从技术上讲,它:
main 函数中接受目标地址(t)和以太币数量(amt)execute 函数,传入上述值
现在,花点时间通过输入交易哈希在 Etherscan 上查看该交易。在下一部分中,我们将深入探讨这些步骤中实际发生的事情,以及我们接下来可以做什么。
随着从我们的智能合约账户的转账成功,让我们深入了解实际发生的事情。
返回 Etherscan,我们查看发生转账的交易哈希:

在 From 字段中,我们看到交易是由不同的地址发起(即 0x6892BEF4aE1b5cb33F9A175Ab822518c9025fc3C),这是 Bundler 处理我们创建的 UserOperation 对象。
To 字段地址(例如 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789)指的是 EntryPoint 合约。该合约是白名单 Bundler 调用的合约,用于执行 UserOperations 的批量操作。
在 To 字段下,你会看到转账情况 - 从 0xD44946...7BA5270F 转出的 0.00000000069379401 ETH;让我们逐一下来看每笔转账:
Transfer 0.00000000069379401 ETH From 0xD44946...7BA5270F To 0x5FF137...026d2789:这是从我们的智能合约账户转移到 EntryPoint 合约的交易Transfer 0.02 ETH From 0xD44946...7BA5270F To 0x115c2A...BE1a1529:这是我们从智能合约地址转移到账户输入的 0.2 ETH。Transfer 0.000000000672934136 ETH From 0x5FF137...026d2789 To 0x6892BE...9025fc3C:这是从 EntryPoint 转移给 Bundler 的费用。在 Input Data 字段中,我们看到 handleOps 方法是由我们的智能合约账户调用的,并传入的数据如下:
Function: handleOps((address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[], address)
## Name  Type  Data
0 ops.sender  address 0xD4494616f04ebd65E407330672c4C5A07BA5270F
0 ops.nonce uint256 0
0 ops.initCode  bytes 0x9406cc6185a346906296840746125a0e449764545fbfb9cf0000000000000000000000001ca0e2981c4abd1c9aa20af4e5142cdf8ac68c4f0000000000000000000000000000000000000000000000000000000000000000
0 ops.callData  bytes 0xb61d27f6000000000000000000000000115c2ac736dc0fe31b8e08e1c7475b08be1a152900000000000000000000000000000000000000000000000000470de4df82000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000
0 ops.callGasLimit  uint256 17475
0 ops.verificationGasLimit  uint256 341795
0 ops.preVerificationGas  uint256 50048
0 ops.maxFeePerGas  uint256 1695
0 ops.maxPriorityFeePerGas  uint256 1649
0 ops.paymasterAndData  bytes 0x
0 ops.signature bytes 0xc9fd2edd94f242be428591627ce921ae1e9aa66497fe7ae76e8c28afe719dc933bb85738173cb3dcc1f901a4e788c088043f9b7d95cac22b42bfb45db916b4f71c
2 beneficiary address 0x6892BEF4aE1b5cb33F9A175Ab822518c9025fc3C实际上,我们发送了一个 UserOperation(如 scripts/transfer.ts 中定义),进入一个 Bundlers 的内存池(根据我们的 RPC URL 设置),然后 Bundlers 处理我们的 UserOperation 对象并创建一个交易,调用 EntryPoint 合约中的 handleOps 函数,有效地执行了从我们的智能合约账户向我们定义的地址的以太转账。
为了挑战你对 ERC-4337 的理解,请查看这个简短的 6 题问答!
🧠知识检查
为了实施 ERC-4337,需要在协议层面进行更改
正确 错误
恭喜你!你已使用 ERC-4337 和 Stackup 创建了智能合约账户,并将资金从你的智能合约地址转移到另一个地址。这个过程看起来可能简单,并且不论有无 ERC-4337 都可以完成,但请注意,你已打开了 ERC-4337 提供的许多其他可能性之门,如赞助 gas 费用和批量交易。现在我们已经完成了一个简单的转账,我们可以探索其他功能!
如果你有任何疑问,请查看 QuickNode Forum 寻求帮助。通过关注我们的 Twitter (@QuickNode) 或 Discord 来获取最新信息。
让我们知道 如果你有任何反馈或新主题请求。我们很乐意听到你的声音。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!