作者:Kong
编辑:Sherry
前言
以太坊即将迎来 Pectra 升级,这无疑是一次意义重大的更新,众多重要的以太坊改进提案将借此契机被引入。其中,EIP-7702 对以太坊外部账户(EOA) 进行了变革性的改造。该提案模糊了 EOA 与合约账户 CA 之间的界限,是继 EIP-4337 之后,朝着原⽣账户抽象迈进的关键一步,为以太坊生态系统带来了全新的交互模式。
目前,Pectra 已在测试网络完成部署,预计不久后便会上线主网。本文将深入剖析 EIP-7702 的实现机制,探讨其可能带来的机遇与挑战,并为不同的参与者提供实用的操作指南。
协议分析
概述
EIP-7702 引入了一种全新的交易类型,它允许 EOA 指定一个智能合约地址,进而为其设置代码。如此一来,EOA 便能够像智能合约一样执行代码,同时还保留了发起交易的能力。这一特性为 EOA 赋予了可编程性与可组合性,用户借此可以在 EOA 中实现诸如社交恢复、权限控制、多签管理、zk 验证、订阅式支付、交易赞助以及交易批处理等功能。值得一提的是,EIP-7702 能够与 EIP-4337 实现的智能合约钱包完美兼容,二者的无缝集成极大地简化了新功能的开发与应用过程。
EIP-7702 的具体实现是引入了交易类型为 SET_CODE_TX_TYPE (0x04) 的交易,其数据结构定义如下:
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 字段定义为:
authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]
在新的交易结构中,除了 authorization_list 字段,其余都遵循与 EIP-4844 相同的语义。该字段是列表类型,列表中可以包含多个授权条目,在每个授权条目中:
- chain_id 字段表示此授权委托所生效的链
- address 字段表示委托的目标地址
- nonce 字段需与当前授权账户的 nonce 相匹配
- y_parity, r, s 字段则是授权账户签署授权的签名数据
在一笔交易内的 authorization_list 字段可以包含多个不同授权账户(EOA) 签署的授权条目,即交易发起者可以与授权者不同,以实现对授权者的授权操作进行 gas 代付。
实现
授权者在签署授权数据时,需要先将 chain_id, address, nonce 进行 RLP 编码。随后将编码后的数据与 MAGIC 数一起进行 keccak256 哈希运算,从而得到待签名的数据[1]。最后,使用授权者的私钥对哈希后的数据进行签名,进而获得 y_parity, r, s 数据。其中,MAGIC (0x05) 是作为域分隔符使用,其目的是确保不同类型签名的结果不会产生冲突。
// Go-ethereum/core/types/tx_setcode.go#L109-L113func (a *SetCodeAuthorization) sigHash() common.Hash { return prefixedRlpHash(0x05, []any{ a.ChainID, a.Address, a.Nonce, })}
需要注意的是,当授权者授权的 chain_id 为 0 时,则代表着授权者允许[2]在所有支持 EIP-7702 的 EVM 兼容链上重放授权(前提是 nonce 也刚好匹配)。
// Go-ethereum/core/state_transition.go#L562if !auth.ChainID.IsZero() && auth.ChainID.CmpBig(st.evm.ChainConfig().ChainID) != 0 { return authority, ErrAuthorizationWrongChainID}
当授权者签署完授权数据后,交易发起者会将其汇聚在 authorization_list 字段中进行签名并通过 RPC 进行交易广播。在交易被包含在区块中执行之前,Proposer 会先对交易进行预检查[3],其中对 to 地址进行强制检查以确保此交易不是合约创建交易,也就是说在发送 EIP-7702 类型的交易时,交易的 to 地址不能为空[4]。
// Go-ethereum/core/state_transition.go#L388-L390if msg.To == nil { return fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From)}
同时,此类交易会强制要求交易中的 authorization_list 字段必须至少包含有一项授权条目,如果有多个授权条目都由同一个授权者签署,那么最终只有最后一个授权条目起效。
// Go-ethereum/core/state_transition.go#L391-L393if len(msg.SetCodeAuthorizations) == 0 { return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From)}
随后,在交易执行过程中,节点会先增加交易发起者的 nonce 数值,再对 authorization_list 中的每个授权条目进行 applyAuthorization 操作。在 applyAuthorization 操作中,节点会先检查授权者的 nonce,然后再增加授权者的 nonce。这意味着如果交易发起者与授权者为同一用户(EOA) 时,在签署授权交易时 nonce 的数值应该再加 1。
// Go-ethereum/core/state_transition.go#L489-L497func (st *stateTransition) execute() (*ExecutionResult, error) { ... st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
// Apply EIP-7702 authorizations. if msg.SetCodeAuthorizations != nil { for _, auth := range msg.SetCodeAuthorizations { // Note errors are ignored, we simply skip invalid authorizations here. st.applyAuthorization(&auth) } } ...}// Go-ethereum/core/state_transition.go#L604func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { authority, err := st.validateAuthorization(auth) ... st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) ...}// Go-ethereum/core/state_transition.go#L566func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) { ... if auth.Nonce+1 auth.Nonce { return authority, ErrAuthorizationNonceOverflow } ...}
在节点应用某个授权条目时,如果遇到任何错误,这条授权条目将被跳过,交易也不会失败,其他授权条目会继续进行应用,以此确保在批量授权场景中不会出现 DoS 风险。
// Go-ethereum/core/state_transition.go#L494for _, auth := range msg.SetCodeAuthorizations { // Note errors are ignored, we simply skip invalid authorizations here. st.applyAuthorization(&auth)}
在授权应用完成后,授权者地址的 code 字段将被设置为 0xef0100 || address,其中 0xef0100 是固定的标识,address 是委托的目标地址。由于 EIP-3541 的限制,使得用户无法通过常规方式部署 0xef 字节开头的合约代码,这就保证了此类标识只能由 SET_CODE_TX_TYPE (0x04) 类型的交易才能部署。
// Go-ethereum/core/state_transition.go#L612st.state.SetCode(authority, types.AddressToDelegation(auth.Address))
// Go-ethereum/core/types/tx_setcode.go#L45var DelegationPrefix = []byte{0xef, 0x01, 0x00}
func AddressToDelegation(addr common.Address) []byte { return append(DelegationPrefix, addr.Bytes()...)}
授权完成后,授权者若要移除授权,只需将委托的目标地址设置为 0 地址即可。
通过 EIP-7702 引入的新的交易类型,使得授权者(EOA) 既可像智能合约一样执行代码,也同时保留了发起交易的能力。相较于 EIP-4337,这为用户带来了更接近原生账户抽象(Native AA) 的体验,极大地降低了用户的使用门槛。
最佳实践
尽管 EIP-7702 为以太坊生态注入了新的活力,但新的应用场景也会带来新的风险。以下是生态参与者在实践过程中需要警惕的方面:
私钥存储
即便 EOA 在委托后可以借助智能合约内置的社交恢复等手段解决因私钥丢失导致的资金损失问题,但它仍然无法避免 EOA 私钥被泄露的风险。需要明确的是,执行委托后,EOA 私钥依旧对账户拥有最高控制权,持有私钥便能够随意处置账户中的资产。用户或者钱包服务商在为 EOA 完成委托后,即便完全删除存储在本地的私钥,也不能完全杜绝私钥泄露风险,尤其是处于存在供应链攻击风险的场景中。
对于用户来说,在使用委托后的账户时,用户仍应该将私钥保护放在首位,时刻注意:Not your keys, not your coins。
多链重放
用户在签署委托授权时,能通过 chainId 选择委托可以生效的链,当然用户也可以选择使用 chainId 为 0 进行委托,这使得委托可以在多链上重放生效,以方便用户一次签名即可在多链上进行委托。但需要注意的是,在多链上委托的同一合约地址中,也可能存在不同的实现代码。
对于钱包服务商来说,在用户进行委托时,应检查委托生效链与当前连接的网络是否相符,并且提醒用户签署 chainId 为 0 的委托可能带来的风险。
用户也应该注意,在不同链上的相同合约地址,其合约代码并不总是相同,应先了解清楚委托的目标。
无法初始化
当前主流的智能合约钱包大多采用代理模型,钱包代理在部署时,会通过 DELEGATECALL 调用实现合约的初始化函数,以此达成钱包初始化与代理钱包部署的原子化操作,避免被抢先初始化的问题。但用户在使用 EIP-7702 进行委托时,仅仅会更新其地址的 code 字段,无法通过调用委托地址进行初始化。这就使得 EIP-7702 无法像常见的 ERC-1967 代理合约一样能在合约部署的交易中调用初始化函数进行钱包初始化。
对于开发者来说,在将 EIP-7702 与现有的 EIP-4337 钱包进行组合适配时,应该注意在钱包的初始化操作中进行权限检查(例如通过 ecrecover 恢复签名地址进行权限检查),以避免钱包初始化操作被抢跑的风险。
存储管理
用户在使用 EIP-7702 委托功能时,可能会因为功能需求变更、钱包升级等原因,需要重新委托到不同的合约地址。但不同合约的存储结构可能存在差异(比如不同合约的 slot0 插槽可能代表不同类型的数据),在重新委托的情况下,有可能导致新合约意外复用旧合约的数据,进而引发账户锁定、资金损失等不良后果。
对于用户来说,应该谨慎处理重新委托的状况。
对于开发者来说,在开发过程中应遵循 ERC-7201 提出的 Namespace Formula,将变量分配到指定的独立存储位置,以缓解存储冲突的风险。此外 ERC-7779 (draft) 还专为 EIP-7702 提供了重新委托的标准流程:包括使用 ERC-7201 防止存储冲突,并在重新委托之前验证存储兼容性,以及调用旧委托的接口清理存储的旧数据。
假充值
用户在进行委托后,EOA 也将可以作为智能合约使用,因此中心化交易所(CEX) 可能会面临智能合约充值普遍化的情况。
CEX 应通过 trace 检查每笔充值交易的状态,防范智能合约假充值风险。
账户转换
在实施了 EIP-7702 委托后,用户的账户类型可以在 EOA 与 SC 之间自由转换,这使得账户既可以发起交易,也可以被调用。意味着当账户调用本身并进行外部调用时,其 msg.sender 也将是 tx.origin,这会打破一些仅限 EOA 参与项目的安全假设。
对于合约开发者来说,假设 tx.origin 始终是 EOA 将不再可行。同样的,通过 msg.sender == tx.origin 检查来防御重入攻击也将失效。
开发者在开发过程中理应假设未来的参与者可能都为智能合约。
合约兼容性
现有的 ERC-721,ERC-777 代币在对合约进行转账时都具有 Hook 功能,这意味着接收者必须实现相应的回调函数以成功接收代币。
对于开发者来说,用户委托的目标合约理应实现相应的回调函数,以确保能够和主流代币兼容。
钓鱼检查
在实施了 EIP-7702 委托后,用户账户中的资产可能都将由智能合约控制,一旦用户将账户委托到了恶意的合约,那么攻击者窃取资金将变得轻而易举。
对于钱包服务商来说,尽快支持 EIP-7702 类型的交易尤为重要,并且在用户进行委托签名时,应向用户着重展示委托的目标合约,以缓解用户可能遭受钓鱼攻击的风险。
此外,对账户委托的目标合约进行更深入的自动分析(开源检查,权限检查等)可以更好地帮助用户避免此类风险。
总结
本文围绕以太坊即将到来的 Pectra 升级中的 EIP-7702 提案展开探讨。EIP-7702 通过引入新的交易类型,使 EOA 具备可编程性与可组合性,模糊了 EOA 与合约账户的界限。由于目前还没有一个经过实战考验的兼容 EIP-7702 类型的智能合约标准,因而在实际应用中,不同的生态参与者,如用户、钱包服务商、开发者、CEX 等,都面临着诸多挑战与机遇。本文所阐述的最佳实践内容无法涵盖所有的潜在风险,但仍值得各方在实际操作中借鉴应用。
示例
[Set EOA Account Code]
https://holesky.etherscan.io/tx/0x29252bf527155a29fc0df3a2eb7f5259564f5ee7a15792ba4e2ca59318080182
[Unset EOA Account Code]
https://holesky.etherscan.io/tx/0xd410d2d2a2ad19dc82a19435faa9c19279fa5b96985988daad5d40d1a8ee2269
相关链接
[1] https://github.com/ethereum/go-ethereum/blob/7fed9584b5426be5db6d7b0198acdec6515d9c81/core/types/tx_setcode.go#L109-L113
[2] https://github.com/ethereum/go-ethereum/blob/7fed9584b5426be5db6d7b0198acdec6515d9c81/core/state_transition.go#L562
[3] https://github.com/ethereum/go-ethereum/blob/7fed9584b5426be5db6d7b0198acdec6515d9c81/core/state_transition.go#L304
[4] https://github.com/ethereum/go-ethereum/blob/7fed9584b5426be5db6d7b0198acdec6515d9c81/core/state_transition.go#L388-L390
参考资料
[EIP-7702]https://eips.ethereum.org/EIPS/eip-7702
[EIP-4844]https://eips.ethereum.org/EIPS/eip-4844
[Go-ethereum]https://github.com/ethereum/go-ethereum/tree/7fed9584b5426be5db6d7b0198acdec6515d9c81
[EIP-3541]https://eips.ethereum.org/EIPS/eip-3541#backwards-compatibility
[Cobo: EIP-7702实用指南]
https://mp.weixin.qq.com/s/ojh9uLw-sJNArQe-U73lHQ
[Viem]https://viem.sh/experimental/eip7702/signAuthorization
[ERC-7210]https://eips.ethereum.org/EIPS/eip-7201
[ERC-7779]https://eips.ethereum.org/EIPS/eip-7779
免责声明:本文章仅代表作者个人观点,不代表本平台的立场和观点。本文章仅供信息分享,不构成对任何人的任何投资建议。用户与作者之间的任何争议,与本平台无关。如网页中刊载的文章或图片涉及侵权,请提供相关的权利证明和身份证明发送邮件到support@aicoin.com,本平台相关工作人员将会进行核查。