为 EOA 设置代码
这篇文章介绍了一种名为 EIP-7702 的新型以太坊交易类型,旨在允许外部账户(EOA)设置自己的执行代码。通过引入授权列表和委托机制,该 EIP 使得 EOA 能够实现交易批处理、费用赞助和权限细分等智能合约功能,显著提升用户体验,并为未来的账户抽象提供了兼容性方案。
摘要
新增一种 EIP-2718 交易类型,允许外部账户(EOA)设置其账户中的代码。这通过将一个授权元组列表——每个元组格式为 $[chain\_id, address, nonce, y\_parity, r, s]$——附加到交易中来实现。对于每个元组,一个委托指示符 $(0xef0100 || address)$ 会被写入授权账户的代码中。所有执行代码的操作都必须加载并执行由该委托指向的代码。
动机
尽管智能合约钱包生态系统取得了巨大进步,但 EOA 阻碍了用户体验(UX)改进在应用程序中的广泛采用。因此,本 EIP 旨在为 EOA 增加短期功能改进,这将使 UX 改进渗透到整个应用程序堆栈中。本 EIP 围绕以下三个特定功能进行设计:
- 批量处理:允许同一用户在一个原子交易中执行多个操作。一个常见的例子是 ERC-20 批准后,再花费该批准。这是去中心化交易所(DEXes)中常见的需要两次交易的工作流程。批量处理的高级用例偶尔涉及依赖关系:第一个操作的输出是第二个操作输入的一部分。
- 赞助:账户 $X$ 代表账户 $Y$ 支付交易费用。账户 $X$ 可以为此服务获得其他 ERC-20 代币的支付,或者它可能是一个免费包含其用户交易的应用程序运营商。
- 权限降级:用户可以签署子密钥并赋予它们特定的权限,这些权限比对账户的全局访问权限弱得多。例如,允许花费 ERC-20 代币但不能花费 ETH,或者每天最多花费总余额的 $1%$,或者只与特定应用程序交互。
规范
参数
| 参数 | 值 |
|---|---|
$SET\_CODE\_TX\_TYPE$ |
$0x04$ |
$MAGIC$ |
$0x05$ |
$PER\_AUTH\_BASE\_COST$ |
$12500$ |
$PER\_EMPTY\_ACCOUNT\_COST$ |
$25000$ |
设置代码交易
引入了一种新的 EIP-2718 交易,称为“设置代码交易”,其中 $TransactionType$ 为 $SET\_CODE\_TX\_TYPE$,$TransactionPayload$ 是以下内容的 RLP 序列化:
rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
destination, value, data, access_list, authorization_list, signature_y_parity,
signature_r, signature_s])
authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]
外部交易的 chain_id、nonce、max_priority_fee_per_gas、max_fee_per_gas、gas_limit、destination、value、data 和 access_list 字段遵循 EIP-4844 的相同语义。请注意,这意味着空 $destination$ 是无效的。
此交易的 $signature\_y\_parity, signature\_r, signature\_s$ 元素表示对 $keccak256(SET\_CODE\_TX\_TYPE || TransactionPayload)$ 的 secp256k1 签名。
$authorization\_list$ 是一个元组列表,表示每个元组的签名者希望在其 EOA 上下文中执行的代码。如果 $authorization\_list$ 的长度为零,则该交易被认为是无效的。
当授权元组中的任何字段超出以下范围时,该交易也被认为是无效的:
assert auth.chain_id < 2**256
assert auth.nonce < 2**64
assert len(auth.address) == 20
assert auth.y_parity < 2**8
assert auth.r < 2**256
assert auth.s < 2**256
此交易的 EIP-2718 $ReceiptPayload$ 是 rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])。
行为
授权列表在交易的执行部分开始之前处理,但在发送者的 nonce 增加之后。
对于每个 $[chain\_id, address, nonce, y\_parity, r, s]$ 元组,执行以下操作:
- 验证链 ID 是 $0$ 还是当前链的 ID。
- 验证 $nonce$ 小于
$2^{64} - 1$。 - 令
$authority = \text{ecrecover}(msg, y\_parity, r, s)$。- 其中
$msg = \text{keccak}(MAGIC || \text{rlp}([chain\_id, address, nonce]))$。 - 验证 $s$ 小于或等于
$\text{secp256k1n}/2$,如 EIP-2 中所规定。
- 其中
- 将 $authority$ 添加到
$accessed\_addresses$,如 EIP-2929 中所定义。 - 验证 $authority$ 的代码为空或已委托。
- 验证 $authority$ 的 nonce 等于 $nonce$。
- 如果 $authority$ 不为空,将
$PER\_EMPTY\_ACCOUNT\_COST - PER\_AUTH\_BASE\_COST$的 gas 添加到全局退款计数器。 - 将 $authority$ 的代码设置为
$0xef0100 || address$。这是一个委托指示符。- 如果 $address$ 是
$0x0000000000000000000000000000000000000000$,则不写入委托指示符。通过将账户的代码哈希重置为空代码哈希$0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470$来清除账户的代码。
- 如果 $address$ 是
- 将 $authority$ 的 nonce 增加一。
如果以上任何步骤失败,立即停止处理当前元组并继续处理列表中的下一个元组。当来自同一授权者的多个元组存在时,使用最后一个有效元组中的地址设置代码。
请注意,如果交易执行失败(例如,任何异常情况或代码回滚),已处理的委托指示符不会回滚。
委托指示符
委托指示符使用 EIP-3541 中定义的禁用操作码 $0xef$ 来指示代码必须以不同于常规代码的方式处理。该委托强制所有执行代码的操作遵循地址指针以获取要执行的代码。例如,CALL 加载 $address$ 处的代码并在 $authority$ 的上下文中执行它。
受影响的执行操作包括:
CALLCALLCODEDELEGATECALLSTATICCALL- 任何 $destination$ 指向存在委托指示符的地址的交易
对于代码读取,只有 CODESIZE 和 CODECOPY 指令受影响。它们直接对执行中的代码而不是委托进行操作。例如,当执行委托账户时,EXTCODESIZE 返回 $23$($0xef0100 || address$ 的大小),而 CODESIZE 返回 $address$ 处代码的大小。
请注意,这意味着在委托执行期间,CODESIZE 和 CODECOPY 产生的结果与调用 $authority$ 上的 EXTCODESIZE 和 EXTCODECOPY 产生的结果不同。
预编译合约
当预编译合约地址是委托的目标时,检索到的代码被视为空,CALL、CALLCODE、STATICCALL、DELEGATECALL 指令针对此账户将执行空代码,因此在给定足够的 gas 启动调用时会成功执行而没有任何执行。
循环
如果委托指示符指向另一个委托,从而创建了潜在的委托链或循环,客户端必须仅检索第一个代码,然后停止跟踪委托链。
Gas 成本
新交易的固有成本继承自 EIP-2930,具体为 $21000 + 16 \times \text{非零 calldata 字节数} + 4 \times \text{零 calldata 字节数} + 1900 \times \text{访问列表存储键计数} + 2400 \times \text{访问列表地址计数}$。此外,还要加上 $PER\_EMPTY\_ACCOUNT\_COST \times \text{授权列表长度}$ 的成本。
无论有效性或重复性如何,交易发送者都将支付所有授权元组的费用。
如果执行代码的指令在解析委托代码期间访问了一个冷账户,则在正常成本上增加额外的 EIP-2929 $COLD\_ACCOUNT\_READ\_COST$ 成本,即 $2600$ gas,并将该账户添加到 $accessed\_addresses$ 中。否则,评估 $WARM\_STORAGE\_READ\_COST$ 成本为 $100$。
交易发起
修改 EIP-3607 设定的限制,允许代码为有效委托指示符(即 $0xef0100 || address$)的 EOA 发起交易。具有任何其他代码值的账户可能无法发起交易。
此外,如果交易的 $destination$ 具有委托指示符,则将委托的目标添加到 $accessed\_addresses$ 中。
基本原理
以下是 EIP 的总体设计方向和特定技术选择的基本原理。
一般设计理念
代码委托的持久性
本提案的初稿有一个巧妙的想法,以避免在协议内撤销是否需要的问题上产生分歧。这个想法是临时设置授权账户中的代码。交易完成后,代码将完全清除。这为丰富 EOA 功能开辟了一个新的设计空间。
即使这种方法也并非没有缺陷。从根本上说,用户包含设置代码授权的摩擦不大。这意味着一些用户和应用程序会选择将此扩展视为一种脚本设施,而不是智能合约钱包的全面升级。其结果将是两个有些竞争的 UX 改进工作流:智能合约钱包和 EOA 脚本。
以前的提案也曾受到类似的批评。为了抵消这一点,引入了持久性委托。它们在部署时产生了足够的摩擦,使得用户不会定期部署新的、独特的委托。这有望统一工作流,并最大限度地减少 UX 开发中的碎片化。
无 initcode
运行 initcode 因多种原因不可取。它创建了一种新的执行模式,需要大量的测试,并且可能用于标准智能合约钱包无法实现的目的。它还迫使开发人员在委托后将初始化作为对 EOA 的标准调用来执行。这些操作缺乏原子性是另一个将促使用户转向完整的智能合约钱包解决方案而不是 EOA 脚本的因素。
此外,initcode 倾向于在交易 calldata 内部传播。这意味着它需要包含在授权元组中并进行签名。最小的 initcode 大约是 $15$ 字节——它将简单地从外部地址复制合约代码。总成本将是 $16 \times 15 = 240$calldata 成本,加上 [EIP-3860](https://learnblockchain.cn/article/24341) 成本$2 \times 15 = 30$,再加上运行时成本大约 $150$。因此,准备账户将花费近 $500$额外的 gas。如果不是从外部账户复制,则更可能是$1200+$` gas。
通过模板创建
无论有没有 initcode,都存在用户应如何指定他们打算在其账户中运行的代码的问题。两个主要选项是直接在交易中指定字节码或指定代码指针。最简单的指针将只是链上部署代码的地址。
成本分析清楚地表明了答案。最小的代理合约大约是 $50$ 字节,地址是 $20$ 字节。$30$ 字节的差异没有提供任何有用的额外功能,并将被低效地复制数十亿次。
此外,直接指定代码将再次使 EOA 能够拥有在交易 calldata 中指定任意代码的新的、独特的能力。正是由于这些原因,选择了通过模板创建。
与应用程序和钱包的交互
虽然这个 EIP 为应用程序和 EOA 提供了很大的灵活性,但也有不正确的使用方式。应用程序绝不能期望它们可以建议用户签署授权,因此钱包有责任不提供执行此操作的界面。
没有安全的方法可以提供此界面。授权指定代码对账户拥有无限制的访问权限,并且必须始终由钱包密切审计。很少有用户具有足够高的复杂程度来合理地验证他们正在委托的代码。
也不可能在此级别实现权限系统以最小化风险。如果应用程序需要自定义钱包功能,它们必须使用基于委托代码构建的标准化扩展/模块系统,并正确实现权限。
与未来账户抽象的向前兼容性
本 EIP 旨在与终局账户抽象向前兼容,而不会过度固化 ERC-4337 或 RIP-7560 的任何细粒度细节。
首先,用户签署的 $address$ 可以直接指向现有的 ERC-4337 钱包代码。这实质上要求所使用的“代码路径”在大多数情况下在纯智能合约钱包世界中仍然有意义。因此,它避免了创建两个独立 UX 工作流的问题,因为在很大程度上,它们将是相同的生态系统。
在此解决方案下,将有一些工作流需要“凑合”,而这些工作流在“终局 AA”下会做得更好,但这只是相对较小的一部分。EIP 不需要添加任何操作码,这些操作码在后 EOA 世界中会变得悬空且无用,并且它允许 EOA 伪装成合约,以与现有 EntryPoint 兼容的方式包含在 ERC-4337 捆绑包中。
自助赞助:允许 $tx.origin$ 设置代码
允许 $tx.origin$ 设置代码并执行其自己的委托代码,实现了所谓的自助赞助。它允许用户利用 EIP-7702,而无需依赖任何第三方基础设施。
然而,这意味着该 EIP 打破了 $msg.sender == tx.origin$ 仅在交易的顶层执行帧中发生的这个不变性。这将影响包含 $\text{require}(msg.sender == tx.origin)$ 样式检查的智能合约。此检查至少用于三个目的:
- 确保 $msg.sender$ 是一个 EOA(因为 $tx.origin$ 始终必须是一个 EOA)。这个不变性不依赖于执行层深度,因此不受影响。
- 防止像闪电贷这样的原子三明治攻击,这些攻击依赖于在同一原子交易中,在目标合约执行之前和之后修改状态的能力。此保护将被此 EIP 打破。然而,以这种方式依赖 $tx.origin$ 被认为是不良实践,并且矿工已经可以通过有条件地包含区块中的交易来规避。
- 防止重入。
在以太坊主网上部署的合约中可以找到(1)和(2)的例子,其中(1)更常见(且不受此提案影响)。另一方面,用例(3)受此提案的影响更严重,但此 EIP 的作者没有找到这种形式的重入保护的例子,尽管搜索并不详尽。
出现情况的这种分布——许多(1)、一些(2)和没有(3)——正是此 EIP 作者所预期的,因为:
- 在没有 $tx.origin$ 的情况下确定 $msg.sender$ 是否为 EOA 是困难的,甚至是不可能的。
- 唯一能避免原子三明治攻击的执行上下文是顶层上下文,而
$tx.origin == msg.sender$是检测该上下文的唯一方法。 - 相比之下,有许多直接且灵活的方法可以防止重入(例如,使用瞬态存储变量)。由于
$msg.sender == tx.origin$仅在顶层上下文为真,因此它将成为一种晦涩的防止重入工具,而不是其他更常见的方法。
还有其他方法可以缓解此限制而不破坏不变性:
- 在使用 EOA 上下文中的
CALL*指令时,将$tx.origin$设置为常量$ENTRY\_POINT$地址。 - 将
$tx.origin$设置为从发送方或签名方地址派生出的特殊地址。 - 禁止
$tx.origin$设置代码。这将使简单的批量用例不可能实现,但将来可以放宽。
技术细节的基本原理
委托成本
$PER\_AUTH\_BASE\_COST$ 是处理授权元组和设置委托目标的成本。为了计算此操作的合理成本,作者回顾了其对系统的影响:
- 传递
$101$ 字节的 calldata =$101 \times \text{非零成本} (16) = 1616$ - 恢复 $authority$ 地址 =
$3000$ - 读取 $authority$ 的 nonce 和代码 =
$2600$ - 在已激活账户中存储值 =
$200$ - 部署代码成本 =
$200 \times 23 = 4600$
基于影响的评估确定该操作的同等计算量为 $12016$ gas。向上取整到 $12500$,以考虑与状态转换中数据传输相关的杂项成本。
清除委托指示符
状态转换更改中的一个通用设计目标是最大限度地减少 EIP 的特殊情况数量。在早期迭代中,本 EIP 抵制了清除账户委托指示符的特殊情况。
在大多数情况下,委托给 $0x0$ 的账户与真正的 EOA 无法区分。然而,一种特别不幸的情况是无法避免的。即使用户的委托指示符归零,与该账户交互的大多数操作在首次接触时仍会因尝试加载 $0x0$ 处的代码而产生额外的 $COLD\_ACCOUNT\_READ\_COST$。
出于这个原因,作者选择包含一个特殊情况,允许用户将其 EOA 恢复到其原始纯度。
缺乏指令禁止
一致性是 EVM 中一个有价值的属性,无论从实现角度还是用户理解角度来看。尽管考虑禁止 EOA 上下文中的几类指令,但作者认为没有令人信服的理由这样做,因为这会导致智能合约钱包和 EOA 智能合约钱包沿着不同的 UX 工作流程发展。
考虑禁止的主要指令家族是与存储相关和与合约创建相关的指令。不禁止存储指令的决定主要取决于它们对智能合约钱包的重要性。虽然可以有一个外部存储合约供智能合约钱包调用,但这会不必要地复杂且低效。未来,新的状态方案可能允许以更低的成本访问账户中的某些存储槽。这是智能合约钱包希望利用的,而存储合约无法支持。
创建指令也被考虑禁止在其他类似的 EIP 中,但是因为本 EIP 允许 EOA 在交易内部花费价值,所以在交易内部增加 nonce 并使待处理交易失效的担忧并不重要。
跨链可塑性保护
签署代码指针时的一个考虑因素是该地址在另一条链上指向什么代码。虽然可以通过确定性部署(即通过 Nick 的方法)创建,但验证此类部署可能并不总是可取的。在这种情况下,可以设置链 ID 以缩小授权范围。当需要通用部署时,只需将链 ID 设置为 $0$。
添加链 ID 的另一种方法是在签名中用实际代码替换地址。这似乎具有以下优点:通过继续只序列化地址,最大限度地减少授权元组的链上大小;同时通过为签名拉取代码,保留实际在账户中运行的代码的特异性。然而,这种格式的一个不幸问题是,它强制进行数据库查找以确定每个授权元组的签名者。这种强制本身似乎在交易传播中造成了足够的复杂性,因此决定避免并直接签署地址。
仅限代码执行的委托
其他代码检索操作,如 EXTCODEHASH,不会自动遵循委托,它们直接对委托指示符本身进行操作。如果委托被遵循,一个账户将能够暂时伪装成具有特定的代码哈希,这将破坏依赖代码哈希作为账户行为定义的合约。合约行为的改变目前只有在其代码明确允许(特别是通过 DELEGATECALL)的情况下才可能,并且代码哈希的改变只有在 SELFDESTRUCT 存在的情况下才可能(自坎昆升级以来,仅适用于与合约创建相同的交易),因此选择在 EXTCODE* 操作码中遵循委托会创建一种新的账户类型,打破先前的假设。
预先收取最大费用
在计算固有 gas 成本时,交易会为每个委托收取最坏情况下的成本。随后,在处理授权列表时,如果账户已存在于状态中,则会发出退款。该机制旨在避免在计算固有 gas 时对每个授权进行状态查找,并且可以仅通过对发送方账户进行状态查找来快速确定交易的有效性。
无 blob,无合约创建
交易应被视为专用工具,不一定是万能解决方案。EIP-4844 在点对点(p2p)层面上受到不同对待,因为 blob 对节点带宽造成负担。EIP-7702 对交易传播有不同的影响,没有必要通过使其成为所有可能功能的超集来不必要地使这些规则复杂化。作者最终预计对原子委托和 blob 提交的需求不会很大。
合约创建是另一个特殊用例,已成为多种交易类型的固有部分。它增加了测试的复杂性,因为它是每次 EVM 发生任何更改时都需要测试的新独立执行分支,并验证该更改在该上下文中是否按预期工作。
出于这些原因,作者选择将 EIP 的范围集中在改善用户体验上。
禁止委托给预编译合约
预编译合约本身就是边缘情况,因此是否允许委托给预编译合约需要在实现上有所关注。考虑到预编译合约的技术上没有与其账户关联的代码,作者决定,当用户委托给预编译合约时,不执行预编译合约逻辑会稍微简单一些。这有些反直觉。
强制要求非空授权列表
设置代码交易必须至少有一个授权才能被视为有效。这是为了阻止发送者将类型 $4$ 交易用作通用交易格式,因为这种交易对交易池的影响与例如 EIP-1559 交易不同。
向后兼容性
本 EIP 破坏了几个不变性:
- 账户余额只能因源自该账户的交易而减少。
- 一旦账户被委托,对该账户的任何调用也可能导致余额减少。
- EOA nonce 在交易执行开始后可能不会增加。
- 一旦账户被委托,该账户可以在执行期间调用创建操作,导致 nonce 增加。
$tx.origin == msg.sender$只能在执行的顶层帧中为真。- 一旦账户被委托,它可以在每个交易中调用多个次。
安全考虑
安全委托合约的实现
以下是委托合约应该警惕并要求账户授权者签署的陷阱的非详尽列表:
- 应由委托方实现并签署重放保护(例如,nonce)。没有它,恶意行为者可以重复使用签名,重复其效果。
$value$-- 没有它,恶意赞助者可能会在被调用方中造成意外效果。$gas$-- 没有它,恶意赞助者可能会导致被调用方耗尽 gas 并失败,从而损害被赞助者。$target$ / $calldata$-- 没有它们,恶意行为者可能会在任意合约中调用任意函数。
一个实现不当的委托可以允许恶意行为者几乎完全控制签名者的 EOA。
抢先初始化
智能合约钱包开发者必须考虑在没有执行的情况下在账户中设置代码的影响。合约通常通过执行 initcode 来部署,以确定要放置在账户中的确切代码。这使得开发者有机会同时初始化存储槽。账户的初始值不能被观察者替换,因为它们要么在创建交易的情况下由 EOA 签署,要么通过从 initcode 的哈希确定性地计算合约地址来提交。
本 EIP 不为开发者提供在委托期间运行 initcode 和设置存储槽的机会。为了保护账户免受观察者抢先用他们控制的账户初始化委托的情况,智能合约钱包开发者必须验证用于设置目的的账户的初始 calldata 是否由 EOA 的密钥使用 ecrecover 签署。这确保账户只能用期望的值进行初始化。
存储管理
更改账户的委托是一项安全关键操作,不应轻率进行,特别是如果新委托的代码并非故意设计和测试为旧代码的升级版。
特别是,为了确保账户从一个委托合约安全迁移到另一个,这些合约必须以一种避免它们之间意外冲突的方式使用存储。例如,使用 ERC-7201 合约可以将其存储布局的根目录设置在一个取决于唯一标识符的槽位。为了简化这一点,智能合约语言可以提供一种重新确定现有合约源代码的整个存储布局根目录的方法。
如果账户先前委托给的所有合约都使用上述方法,则迁移不应造成任何问题。但是,如果有任何疑问,建议首先清除所有账户存储,这是一个协议不原生提供的操作,但可以通过专门的委托合约来实现。
将代码设置为 $tx.origin$
允许 EIP-7702 的发送方也设置代码可能会:
- 破坏依赖
$tx.origin$的原子三明治攻击保护; - 破坏
$\text{require}(tx.origin == msg.sender)$风格的重入保护。
本 EIP 的作者认为允许这样做的风险是可以接受的,原因已在基本原理部分概述。
赞助交易中继者
authorized 账户可能会导致赞助交易中继者在未获得报酬的情况下花费 gas,这可能是由于授权失效(即账户的 nonce 增加)或将相关资产从账户中取出。中继者在设计时应考虑到这些情况,可能需要要求存入保证金或实施信誉系统。
交易传播
允许 EOA 通过委托指示符表现为智能合约,给交易传播带来了一些挑战。传统上,EOA 只能通过交易发送价值。这种不变性允许节点静态地确定该账户交易的有效性。换句话说,单笔交易只能使发送方账户待处理的交易失效。
有了这个 EIP,就有可能导致其他账户的交易变得过时。这是因为一旦 EOA 委托了代码,任何人都可以随时在交易中调用该代码。因此,静态地得知账户余额是否已被清空变得不可能。
虽然有一些缓解措施,但作者建议客户端对于任何具有非零委托指示符的 EOA,不要接受超过一笔待处理交易。这最大限度地减少了单笔交易可能使失效的交易数量。
另一种选择是扩展 EIP-7702 交易,包含一个调用者希望在交易期间“激活”的账户列表。这些账户仅对包含在列表中的 EIP-7702 交易表现为委托代码,从而使客户端能够静态分析和推断待处理交易。
一个相关问题是 EOA 的 nonce 可能在一次交易中增加多次。由于客户端在更糟糕的情况下(如上所述)已经需要具有鲁棒性,因此这不是一个主要问题。然而,客户端应意识到这种行为是可能的,并相应地设计其交易传播。
版权
通过 CC0 放弃版权及相关权利。
- 原文链接: github.com/nerolation/EI...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~