本文深入探讨了ERC-4337中的智能账户和智能合约钱包。文章首先回顾了钱包的创建和验证过程,强调了用户在使用智能合约钱包时的灵活性和安全性。文中介绍了ERC-4337的设计原则及其推动用户无缝体验的重要性,最后总结了智能合约钱包的优势及其未来发展方向。
在这篇文章中,我们继续探讨 ERC-4337:通过替代内存池的账户抽象。在 第 1 部分 中,我们介绍了 Bundler,该组件负责监听用户操作 (UserOperations),对其进行打包并发送作为常规交易到以太坊节点。在 第 2 部分 中,我们深入探讨了 EntryPoint,这是一个关键实体,用于验证和执行用户操作。
接下来,我们将重点关注 智能账户、智能合约钱包,或者简单称为 钱包,这是 ERC-4337 所需组件中的最后一个。在后续的文章中,我们将介绍 Paymasters 扩展,这是一种可选组件,可以为用户赞助交易。
来源:以太坊钱包的今天和明天 — EIP-3074 vs. ERC-4337
ERC-4337 的一个主要目标是使用户能够使用具有自定义验证逻辑的智能合约钱包作为其主要账户,从而消除用户维护外部拥有账户 (EOAs) 的需要。
为此,一个重要的设计目标是复制 EOAs 的关键属性,即用户无需执行任何自定义操作即可创建钱包。他们只需在本地生成一个地址,并立即开始接受资金。因此,钱包的创建由一个“工厂”合约完成,通过使用 CREATE2 在一个逆因果(确定性)地址中创建钱包。
钱包的创建通过 UserOperation 结构体 的 initCode
字段进行:
/**
* 用户操作结构
* @param sender - 此请求的发送账户。
* @param nonce - 发送者用来验证其不是重播的唯一值。
* @param initCode - 如果设置,该账户合约将通过此构造函数创建/
* @param callData - 在此账户上执行的方法调用。
* @param callGasLimit - 传递给 callData 方法调用的气体限制。
* @param verificationGasLimit - 用于 validateUserOp 和 validatePaymasterUserOp 的气体。
* @param preVerificationGas - 该气体未由 handleOps 方法计算,但添加到已支付的气体中。
* 涵盖批处理开销。
* @param maxFeePerGas - 同 EIP-1559 气体参数。
* @param maxPriorityFeePerGas - 同 EIP-1559 气体参数。
* @param paymasterAndData - 如果设置,此字段包含 paymaster 地址和特定于 paymaster 的数据。
* paymaster 将支付交易费用,而不是发送者。
* @param signature - 发送者对整个请求的验证签名,包括 EntryPoint 地址和链 ID。
*/
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
当它的长度不为零时,它被解析为一个 20 字节的地址(工厂),后面跟着“调用数据”(calldata)传递给这个地址。调用数据包含编码的函数签名和参数,调用时,期望会创建一个钱包(如果不存在)并返回其地址(在所有情况下)。这样可以使客户端在不知道钱包是否已经部署的情况下轻松查询地址,通过模拟对 entryPoint.getSenderAddress()
的调用来实现,该调用在底层调用了工厂。
account-abstraction/contracts/core/SenderCreator.sol – Medium
contractSenderCreator {
/**
\* 调用 "initCode" 工厂以创建并返回发送者账户地址。
\* @param initCode - UserOp 中的 initCode 值,包含 20 字节的工厂地址,
\* 后面跟着调用数据。
\* @return sender - 创建的账户返回地址,或在失败时返回零地址。
*/
function createSender(
bytescalldatainitCode
) externalreturns (address sender) {
address factory =address (bytes20(initCode[0:20]));
bytesmemory initCallData = initCode[20:];
bool success;
/\* solhint-disable no-inline-assembly */
assembly {
success :=call(
gas(),
factory,
0,
add(initCallData, 0x20),
mload(initCallData),
0,
32
)
sender :=mload(0)
}
if (!success) {
sender =address (0);
}
}
}
工厂方法创建的钱包合约应实现 IAccount,该接口仅包含一个函数:validateUserOp
。该方法验证用户操作的签名,这是 EntryPoint 执行钱包账户上的用户操作所必需的。
这很有趣,因为尽管 Infinitism 的库 提供了 IAccount 接口的 BaseAccount 实现和 SimpleAccount 示例合约(由 SimpleAccountFactory 创建),但这些都不是规范的实际要求。开发者可以根据 ERC 的要求自由自定义钱包和工厂的实现,前提是他们正确执行必要的验证。然而,解决适当的安全考虑是至关重要的,因为许多安全评审已在这些合约的自定义实现中识别出了严重的漏洞。
在 EntryPoint 执行 handleOps
时,验证用户操作的气体限制和钱包预付款 通过 _validatePrepayment
。这将在内部调用 _validateAccountPrepayment
,如果需要在 _createSenderIfNeeded
上创建账户,并在其上执行 validateUserOp
函数,如果 paymaster 不存在,则将缺少的账户资金(所需气体预存款与当前账户存款之间的差额)作为参数传递。
可以通过 depositTo
进行存款,这意味着任何逆因果地址都可以在创建之前就具备执行用户操作的能力。我们可以想象一个服务提供商允许法币流入 ERC-4337 钱包,通过代表钱包账户直接存款,比如。
当 missingAccountFunds
非空时,钱包账户需要向 EntryPoint 转账以太,正如在 validateUserOp
的 BaseAccount 实现中可以看到的那样,通过 _payPrefund
。
account-abstraction/develop/contracts/core/BaseAccount.sol – Medium
abstract contractBaseAccountisIAccount {
// ...
/\*\*
\\* 向入口点 (msg.sender) 发送此交易所需的缺少资金。
\\* 子类可以重写此方法以实现更好的资金管理
\\* (例如,发送给 EntryPoint 的资金多于最低要求,以便在未来交易中
\\* 不再需要再次发送)。
\\* @param missingAccountFunds - 此方法应向入口点发送的最小值。
\\* 此值可以为零,如果存款足够,
\\* 或用户操作有一个 paymaster。
\*/
function _payPrefund(uint256missingAccountFunds) internalvirtual {
if (missingAccountFunds !=0) {
(boolsuccess, ) =payable(msg.sender).call{
value: missingAccountFunds,
gas: type(uint256).max
}("");
(success);
// 忽略失败(验证是 EntryPoint 的工作,不是账户的)。
}
}
}
在验证预付款和 账户 nonce(在 v0.6 中引入)之后,EntryPoint 最终以指定的气体限制对钱包合约执行任意调用。在 SimpleAccount 中,这将是 execute 或 executeBatch。
account-abstraction/develop/contracts/samples/SimpleAccount.sol – Medium
contract SimpleAccountisBaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
// ...
/\*\*
\\* 执行交易(直接从所有者,或通过入口点调用)
\*/
function execute(address dest, uint256value, bytescalldatafunc) external {
_requireFromEntryPointOrOwner();
_call(dest, value, func);
}
/\*\*
\\* 执行一系列交易
\\* @dev 为了减少 trivial case (no value) 的Gas消耗,使用零长度数组表示零值
\*/
function executeBatch(address [] calldatadest, uint256[] calldatavalue, bytes[] calldatafunc) external {
_requireFromEntryPointOrOwner();
require(dest.length== func.length&& (value.length==0 || value.length== func.length), "数组长度不正确");
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]);
}
}
}
// ...
}
ERC-4337 建议账户使用 DELEGATECALL 转发合约来提高Gas效率并启用钱包的可升级性。
账户代码应嵌入一个固定的入口点,以提高Gas效率,并且在引入新的入口点时,用户仍然能够更新其账户的代码地址为新的地址。
在 SimpleAccount 示例合约中,这是一种 UUPS 可升级 合约:
account-abstraction/contracts/samples/SimpleAccount.sol – Medium
contract SimpleAccountisBaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
// ...
function _onlyOwner() internal view {
// 直接由 EOA 所有者,或通过账户自身(通过 execute 重定向)
require(msg.sender== owner || msg.sender==address (this), "仅限所有者");
}
// ...
function _authorizeUpgrade(address newImplementation) internal view override {
(newImplementation);
_onlyOwner();
}
}
_authorizeUpgrade
方法允许账户 owner
通过直接的以太坊交易或通过自己的钱包合约 address (this)
升级合约,后者在通过入口点的用户操作中发生。
在这篇文章中,我们揭示了在 ERC-4337:通过替代内存池的账户抽象背景下 智能账户、智能合约钱包,或简单称为钱包 的运作机制。该组件使用户能够利用具有 自定义验证和任意执行逻辑 的智能合约钱包作为其主要账户,无需传统的外部拥有账户(EOAs),提供了更无缝和用户友好的体验。
展望未来,我们的下一个第 4 部分将深入探讨 Paymasters 扩展,它在用户操作赞助中发挥作用。请继续关注我们揭示 ERC-4337 的其余部分,如果你对安全使用账户抽象有任何疑问,欢迎 联系 我们。
- 原文链接: medium.com/oak-security/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!