EIP-7702: 为 EOA 设置代码
添加一种新的交易类型,可以永久地为 EOA 设置代码
Authors | Vitalik Buterin (@vbuterin), Sam Wilson (@SamWilsn), Ansgar Dietrichs (@adietrichs), lightclient (@lightclient) |
---|---|
Created | 2024-05-07 |
Requires | EIP-2, EIP-161, EIP-1052, EIP-2718, EIP-2929, EIP-2930, EIP-3541, EIP-3607, EIP-4844 |
Table of Contents
摘要
添加一个新的 EIP-2718 交易类型,允许外部拥有账户 (EOA) 设置其账户中的代码。这是通过将授权元组的列表(每个元组单独格式化为 [chain_id,
address, nonce, y_parity, r, s]
)附加到交易来实现的。对于每个元组,一个委托指示符 (0xef0100 || address)
被写入到授权账户的代码中。所有执行代码的操作都必须加载并执行由委托指向的代码。
动机
尽管智能合约钱包生态系统取得了巨大的进步,但 EOA 阻碍了跨应用程序的 UX 改进的广泛采用。因此,本 EIP 侧重于为 EOA 添加短期功能改进,这将使 UX 改进能够渗透到整个应用程序堆栈中。本 EIP 围绕以下三个特定特性设计:
- 批处理:允许来自同一用户的多个操作在一个原子交易中完成。一个常见的例子是 ERC-20 授权,然后是花费该授权。这是 DEX 中常见的需要两个交易的工作流程。批处理的高级用例偶尔会涉及依赖关系:第一个操作的输出是第二个操作的输入的一部分。
- 赞助:账户 X 代表账户 Y 支付交易费用。账户 X 可以通过某种其他 ERC-20 来为此服务获得报酬,或者它可以是一个应用程序运营商,免费包含其用户的交易。
- 权限降级:用户可以签署子密钥,并赋予它们比全局访问账户弱得多的特定权限。例如,花费 ERC-20 代币而不是 ETH 的权限,或者每天花费不超过总余额的 1% 的权限,或者仅与特定应用程序交互的权限。
规范
参数
Parameter | Value |
---|---|
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 相同的语义。注意,这意味着空目标无效。
此交易的 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 = ecrecover(msg, y_parity, r, s)
。- 其中
msg = keccak(MAGIC || rlp([chain_id, address, nonce]))
。 - 验证
s
是否小于或等于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
来清除账户的代码。
- 如果
- 将
authority
的 nonce 增加一。
如果上述任何步骤失败,请立即停止处理元组并继续处理列表中的下一个元组。当来自同一授权机构的多个元组存在时,使用最后一个有效事件中的地址设置代码。
请注意,如果交易执行失败(例如,任何异常情况或代码还原),则已处理的委托指示符不会回滚。
委托指示符
委托指示符使用 EIP-3541 中定义的禁止操作码 0xef
,以指示代码必须以不同于常规代码的方式处理。委托强制所有执行代码的操作遵循地址指针以获取要执行的代码。例如,CALL
加载 address
处的代码,并在 authority
的上下文中执行它。
受影响的执行操作包括:
CALL
CALLCODE
DELEGATECALL
STATICCALL
- 任何
destination
指向存在委托指示符的地址的交易
对于代码读取,只有 CODESIZE
和 CODECOPY
指令会受到影响。它们直接对执行代码而不是委托进行操作。例如,当执行委托账户时,EXTCODESIZE
返回 23
(0xef0100 || address
的大小),而 CODESIZE
返回驻留在 address
处的代码的大小。
请注意,这意味着在委托执行期间,与在授权机构上调用 EXTCODESIZE
和 EXTCODECOPY
相比,CODESIZE
和 CODECOPY
会产生不同的结果。
预编译合约
当预编译地址是委托的目标时,检索到的代码被视为空,并且目标为该账户的 CALL
、CALLCODE
、STATICCALL
、DELEGATECALL
指令将执行空代码,因此在提供足够的 gas 以启动调用时,执行将成功但无任何操作。
循环
如果委托指示符指向另一个委托,从而创建潜在的委托链或循环,则客户端必须仅检索第一个代码,然后停止遵循委托链。
Gas 成本
新交易的内在成本继承自 EIP-2930,具体而言是 21000 + 16 * non-zero calldata bytes +
4 * zero calldata bytes + 1900 * access list storage key count + 2400 * access
list address count
。此外,还需要增加 PER_EMPTY_ACCOUNT_COST *
authorization list length
的成本。
交易发送者将支付所有授权元组的费用,无论其有效性或重复情况如何。
如果在解析委托 코드 时,代码执行指令访问了冷账户,则向正常成本添加额外的 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 脚本。
之前的提案也受到了类似的批评。为了应对这种情况,引入了持久委托。它们在部署中产生了足够的阻力,以至于用户不会定期部署新的、独特的委托。希望这能统一工作流程,并最大限度地减少用户体验开发中的碎片化。
没有 initcode
由于许多原因,运行 initcode 是不可取的。它创建了一种新的执行模式,需要进行广泛的测试,并且可能被用于标准智能合约钱包无法实现的目的。它还迫使开发者在委托后执行初始化,作为对 EOA 的标准调用。这些操作缺乏原子性是另一个将用户推向完成智能合约钱包解决方案的因素,而不是 EOA 脚本。
此外,initcode 往往会在交易 calldata 中传播。这意味着它需要包含在授权元组中并进行签名。最小的 initcode 大约是 15 字节——它只会从外部地址复制合约代码。总成本将是 16 * 15 = 240
calldata 成本,加上 EIP-3860 成本 2 * 15 = 30
,再加上大约 150
的运行时成本。因此,准备账户将花费近 500
额外的 gas。如果不是从外部账户复制,甚至更可能是 1200+ gas。
通过模板创建
无论有没有 initcode,都有一个问题是用户应该如何指定他们打算在其账户中运行的代码。两个主要选项是在交易中直接指定 bytecode,或者指定指向代码的指针。最简单的指针将只是部署在链上的代码的地址。
成本分析使答案变得清晰。最小的代理大约是 50 字节,而地址是 20 字节。这 30 字节的差异没有提供任何有用的附加功能,并且会被低效地复制数十亿次。
此外,直接指定代码将再次使 EOA 能够执行在交易 calldata 中指定的任意代码。正是由于这些原因,才选择了通过模板创建。
与应用程序和钱包的交互
虽然这个 EIP 为应用程序和 EOA 提供了很大的灵活性,但也有一些不正确的使用方式。应用程序不得期望他们可以建议用户签署授权,因此钱包有责任不提供这样做的接口。
没有安全的方法来提供此接口。授权指定的代码可以不受限制地访问账户,并且始终必须由钱包仔细审核。很少有用户具有足够的复杂程度来合理地验证他们委托的代码。
在这个级别上实现权限系统以最大限度地降低风险也是不可能的。如果应用程序需要自定义钱包功能,它们必须使用构建在正确实现权限的委托代码之上的标准化扩展/模块系统。
与未来账户抽象的前向兼容性
此 EIP 旨在与 endgame 账户抽象前向兼容,而不会过度保障 ERC-4337 或 RIP-7560 的任何细粒度细节。
首先,用户签名的 address
可以直接指向现有的 ERC-4337 钱包代码。这本质上要求使用的“代码路径”是在纯智能合约钱包世界中在大多数情况下会继续有意义的代码路径。因此,它避免了创建两个单独的 UX 工作流程的问题,因为在很大程度上,它们将是相同的生态系统。
在某些工作流程中,需要在此解决方案下进行修改,而在“endgame AA”下的某些不同“更原生”状态下可以更好地完成,但这相对较小。该 EIP 不需要添加任何在 EOA 世界中会变得悬空和无用的操作码,并且它允许 EOA 伪装成合约以包含在 ERC-4337 包中,其方式与现有的 EntryPoint
兼容。
自行赞助:允许 tx.origin
设置代码
允许 tx.origin
设置代码并执行其自身的委托代码可以实现所谓的自行赞助。它允许用户利用 EIP-7702,而无需依赖任何第三方基础设施。
但是,这意味着 EIP 打破了 msg.sender == tx.origin
仅发生在交易的最顶层执行框架中的不变量。这将影响包含 require(msg.sender == tx.origin)
样式检查的智能合约。此检查至少用于三个目的:
- 确保
msg.sender
是 EOA(因为tx.origin
始终必须是 EOA)。此不变量不依赖于执行层深度,因此不受影响。 - 防止原子三明治攻击,如闪电贷,这依赖于在同一原子交易中修改目标合约执行前后状态的能力。此保护将被此 EIP 打破。但是,以这种方式依赖
tx.origin
被认为是糟糕的做法,并且矿工已经可以通过有条件地在区块中包含交易来规避它。 - 防止重入。
(1)和(2)的例子可以在部署在 Ethereum 主网上的合约中找到,其中(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 * 非零成本 (16) = 1616
- 恢复
authority
地址 =3000
- 读取
authority
的 nonce 和代码 =2600
- 将值存储在已预热的账户中 =
200
- 部署代码的成本 =
200 * 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
的情况下才有可能更改代码哈希(截至 Cancun,仅适用于与合约创建相同的交易),因此选择在 EXTCODE*
操作码中遵循委托将创建一种新型的违反先前假设的账户。
预先收取最大成本
在计算内在 gas 成本时,交易将收取每次委托的最坏情况成本。稍后,在处理授权列表时,如果该账户已存在于状态中,则会发出退款。此机制旨在避免在计算内在 gas 时对每个授权进行状态查找,并且可以仅通过在发送者账户上进行状态查找来快速确定交易的有效性。
没有 blobs,没有合约创建
交易应被视为专用工具,而不一定是万能的解决方案。由于 blobs 对节点带宽的影响,EIP-4844 在 p2p 级别上受到不同的对待。EIP-7702 对交易八卦有不同的影响,并且没有必要通过使其成为所有可能功能的超集来不必要地复杂化这些规则。作者最终并不期望对原子委托和 blob 提交有太多需求。
合约创建是另一种已添加到几种交易类型中的专门用例。它增加了测试的复杂性,因为它是一个新的独特执行分支,需要在对 EVM 进行任何更改时进行测试,并验证更改是否在该上下文中按预期工作。
由于这些原因,作者选择将 EIP 的范围重点放在改进 UX 上。
不允许委托给预编译合约
预编译合约本身就是边缘情况,因此是否允许委托给预编译合约需要在实现中有所关注。考虑到预编译合约在技术上没有与其账户相关的代码,作者认为当用户委托给预编译合约时,不执行预编译合约逻辑会稍微简单一些。这有点违反直觉。
需要非空授权列表
设置代码交易必须至少有一个授权才能被认为是有效的。这是为了阻止发送者将 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
的原子三明治保护; - 打破
require(tx.origin == msg.sender)
样式的重入保护。
此 EIP 的作者认为,对于允许这样做的风险是可以接受的,原因是在原理部分概述。
赞助的交易中继器
authorized
账户可能会导致赞助的交易中继器花费 gas,而无法通过使授权无效(即,增加账户的 nonce)或通过从账户中清除相关资产来获得偿还。中继器应考虑到这些情况进行设计,可能需要存入保证金或实施信誉系统。
交易传播
允许 EOA 通过委托指示符表现为智能合约,这对交易传播构成了一些挑战。传统上,EOA 只能通过交易发送价值。此不变量允许节点静态地确定该账户的交易的有效性。换句话说,单个交易只能使来自发送者账户的未决交易无效。
有了这个 EIP,就有可能导致来自其他账户的交易变得过时。这是由于一旦 EOA 委托给代码,任何人都可以随时在交易中调用该代码。不可能以静态方式知道账户的余额是否已被清除。
虽然对此有一些缓解措施,但作者建议客户端不要接受任何具有非零委托指示符的 EOA 的一个以上的未决交易。这将最大限度地减少可以由单个交易使其无效的交易数量。
另一种方法是使用调用者希望在交易期间“补充”的账户列表扩展 EIP-7702 交易。这些账户的行为仅适用于包含它们的 EIP-7702 交易的委托代码,从而使客户端能够静态地分析和推理未决交易。
一个相关的问题是,EOA 的 nonce 可能会在每次交易中递增多次。由于客户端已经需要在更糟糕的情况下保持健壮(如上所述),因此这不是一个主要问题。但是,客户端应该意识到这种行为是可能的,并相应地设计其交易传播。
版权
版权及相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Vitalik Buterin (@vbuterin), Sam Wilson (@SamWilsn), Ansgar Dietrichs (@adietrichs), lightclient (@lightclient), "EIP-7702: 为 EOA 设置代码," Ethereum Improvement Proposals, no. 7702, May 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7702.