以太坊 - 账户抽象和ERC-4337 - 第二部分 - Quicknode

  • QuickNode
  • 发布于 2024-04-10 11:10
  • 阅读 26

本文深入探讨了ERC-4337(账户抽象)及其在以太坊中的应用,提供了创建和部署ERC-4337符合的智能合约的具体步骤,包括使用Stackup SDK的实用指南。文章回顾了账户抽象的基本概念,细述了ERC-4337中的关键组件,如UserOperations和Bundlers,并提供了详细的代码示例和操作指导,帮助读者更好地理解和应用这一技术。

概述

在我们的 Account Abstraction and ERC-4337 - Part 1 指南中,我们为理解 EIP-4337 奠定了基础。在本后续指南中,我们将进行实践,深入探讨使用 Stackup 创建和部署与 ERC-4337 兼容的智能合约的具体步骤。准备好深入探索了吗?让我们开始吧!

你需要的准备

你将要做的事情

  • 回顾你对账户抽象(Account Abstraction)(ERC-4337)的理解
  • 使用 Stackup 创建一个 ERC-4337 智能合约
  • 使用智能合约账户和 ERC-4337 发送交易

重新审视以太坊中的账户抽象:ERC-4337 基础知识

什么是账户抽象(ERC-4337)?

在我们深入研究 ERC-4337 的实现之前,让我们重温本指南系列的 第 1 部分 中提到的以太坊账户抽象的基本概念:

  • 以太坊账户挑战:我们讨论了以太坊账户系统当前存在的问题。外部拥有账户(Externally Owned Accounts,EOA)在用户体验上有局限性,特别是在与智能合约交互、执行多步骤操作或管理种子短语时。智能合约账户提供了一些解决方案,但也有自己的挑战。
  • ERC-4337 的介绍: ERC-4337 通常被称为 Account Abstraction Using Alt Mempool,它应运而生,成为上述挑战的一个有前景的解决方案。这个以太坊改进提案(EIP)旨在增强钱包用户的体验。

ERC-4337 的关键组件

  • UserOperations:这是用户想要进行的操作。可能是从智能合约账户转移资金、与另一个智能合约交互或进行社交恢复调用。UserOperation 对象与我们在以太坊中看到的交易对象具有类似的字段。然而,nonce 和签名等字段是账户特定的(由 ERC-4337 实现)。
  • Bundlers:将用户操作收集并提交到以太坊网络的白名单实体,通过 EntryPoint 合约进行。由于 Bundlers 有激励措施保持活跃,它们会收取费用并优先选择哪些 UserOperation 进行打包以实现最大利润。
  • EntryPoint:一个单一的智能合约,用于验证和执行 UserOperations。网络上大多数或所有 Bundlers 将与该合约交互,以发送批量的 UserOperation 对象。
  • 合约账户:这些是由实体控制的合约账户。
  • Paymaster:可选的实体,可以赞助交易费用(例如,另一个实体可以为你的交易费用付款)。
  • Aggregators:帮助验证多个 UserOperations 的签名。

现在我们已经刷新了对 ERC-4337 概念的记忆,让我们深入探讨如何构建和与 ERC-4337 兼容的智能合约进行交互。

探索 SimpleAccount.sol: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

两个主要功能 executeexecuteBatch 允许 owner 或中继系统的入口点发送交易或一系列交易。两个功能会首先检查发送者是否为 EntryPoint 或拥有者,然后进行处理。

该合约还支持将拥有者升级到新拥有者,但任何 EntryPoint 的更新(例如,_entryPoint)都需要部署一个新的智能合约账户。

现在,为了简化处理,我们将使用 Stackup 的 SDK 部署一个 ERC-4337 兼容的合约,并开始执行诸如批准 ERC-20 代币和转移以太币及代币等操作。

使用 Stackup 开发 ERC-4337 智能合约

我们将在本节中开发的 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 Key
  • signingKey:用于生成 UserOperation 签名的密钥。它也被合约账户用于验证交易
  • paymaster.rpcUrl:用于生成 UserOperation 签名的密钥。该签名被合约账户用于验证交易
  • paymaster.context:与你正在交互的 paymaster 相关的任意字段

创建一个 Stackup API Key

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。

QuickNode Multi-Chain Faucet

还可以找到一个备用的 Sepolia Faucet 这里。使用时请小心。

从 SimpleAccount 向另一个地址发起转账

在为我们的智能合约账户(例如 SimpleAccount)提供资金后,我们现在可以从我们的智能合约账户发起转账。我们建议至少有 0.01 ETH 以测试 ETH 转账(加上 gas 费用)。在终端窗口中粘贴以下命令,但请记住用实际值替换占位符,例如 addresseth

yarn run simpleAccount transfer --to {address} --amount {eth}

若要查看运行此命令时执行的代码,请导航到 scripts/simpleAccount/transfer.ts 文件。

简单来说,上述命令部署 SimpleAccount 合约,创建一个 UserOperation 有效载荷,签名后将其发送给 Bundler(如 config.json 中定义)。

从技术上讲,它:

  • main 函数中接受目标地址(t)和以太币数量(amt
  • 然后函数将检查中间件(如果有 paymaster)
  • 使用给定的 config.json 设置初始化 SimpleAccount 合约
  • 解析地址和金额值
  • 签名并调用 execute 函数,传入上述值
  • 返回 UserOperation 哈希和交易哈希

从智能合约账户转账 - ERC-4337

现在,花点时间通过输入交易哈希在 Etherscan 上查看该交易。在下一部分中,我们将深入探讨这些步骤中实际发生的事情,以及我们接下来可以做什么。

分析智能合约账户交易的生命周期

随着从我们的智能合约账户的转账成功,让我们深入了解实际发生的事情。

返回 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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