本文深入探讨了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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!