【技术分析】EIP-7702 场景下 EOA 授权签名的安全探讨
EIP-7702
在 2025 年即将到来的以太坊 Pectra 升级中,将会引入 EIP-7702 这个提案。其主要的内容就是使得 EOA 账户拥有了自己的 Storage ,并且可以通过 delegate 的方式指定一个合约地址作为 EOA 的 implement 合约。总的来说就是 EOA 拥有了 “Proxy” 的功能。除此之外,EOA 还能够进行更换 implement 合约。但目前协议级别不支持清空 Storage 操作,需要通过特定的合约来清空 Storage 。
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md
目前在 7702 场景下主流的开发思路是将 EOA 账户扩展成拥有直接调用、授权签名调用、外部调用等功能的超级 EOA(把 ERC-4337 抽象账户的功能直接在 EOA 上面实现)。其中,授权签名调用是本篇文章想要着重讨论的场景,接下来将会结合 EIP-712 签名方案进行讨论。
EIP-712
EIP-712 作为一个被广泛使用的标准,除了它能够提供格式化便于解析的签名内容外,还因为它在签名内容中加入了合约地址及链 id,能够防止签名被跨合约或跨链重放攻击。
当生成签名内容 digest 时,EIP-712 会将 TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this) 这些内容打包进去。其中,TYPE_HASH 是一个常量,而 _hashedName 和 _hashedVersion 由合约进行配置。

在 7702 场景下使用 712 签名
下面这个合约实现了一个简单的验签功能,通过 validateSignature() 可以验证 signature 的签名者是否为 address(this)。
contract SimpleImp is EIP712 {
constructor() EIP712("NAME", "VERSION") {}
function validateSignature(bytes32 digest, bytes memory signature) public view {
address signer = ECDSA.recover(digest, signature);
require(signer == address(this), "Invalid signature");
}
function getDigest(uint256 message) public view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(message)));
}
}
这在此前的场景下无法正常运行,因为合约地址没有私钥,所以无法对消息进行签名。但是在 7702以后,EOA 可以将 SimpleImp 合约作为实现合约,在验签时如果消息的签名者是 EOA ,则能够通过 require(signer == address(this)) 的验证了。

至此,SimpleImp 合约实现了基本的签名验证功能,但存在签名重放风险:同一个签名可以在协议中被多次使用。为了解决这个问题,可以通过引入 nonce 机制来解决这个问题。通过将 nonce 值纳入签名内容中,并在每次验证签名成功后将 nonce 值加 1,就能确保每个签名只能在合约中使用一次。
在 stateless 7702 场景下使用 712 签名
7702 场景下的 Storage 冲突问题
在上一节中提到可以通过添加 nonce 的方法来避免签名重放,就如下面的代码一样,添加一个全局变量 nonce,每次使用后 nonce 的值加 1 避免签名被重复使用。
但是在 7702 场景下,这个方案实施起来就有点“尴尬”。
contract SimpleImp is EIP712 {
uint256 nonce;
constructor() EIP712("NAME", "VERSION") {}
function validateSignature(bytes memory signature) public view {
bytes32 digest = getDigest(nonce);
address signer = ECDSA.recover(digest, signature);
require(signer == address(this), "Invalid signature");
nonce++;
}
function getDigest(uint256 nonce) public view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(nonce)));
}
}
在介绍 7702 的章节提到,EOA 能够更换 implement 合约,这使得用户可以根据自己的喜好、收益率、功能实现等需求去切换不同的协议。
虽然 7702 给每个 EOA 都增加了 Storage 进行数据的存储,但是由于用户在切换 implement 合约时 Storage 的内容是不会重置的,这也就使得前一个合约使用过的 Storage 会被后一个合约所继承。如果毫无顾及地肆意修改 Storage,可能会导致关键的 slot 发生冲突。
比如,在 OldImp 中 slot0 的值为 nonce = 1,而更新到了 NewImp 时,变成了 owner = 1。

以上的场景显然不是开发者和用户希望看到的,也就导致了开发者们“有 Storage 不敢用”的尴尬局面。
Stateless 7702
不过没关系,虽然不能直接使用 EOA 的 Storage,聪明的开发者们想到了办法:可以通过 Create2 的方法外挂一个 NonceStorage 合约对 nonce 进行存储。

在 ImplementV1 合约中通过 Create2.deploy() 部署了一个 NonceStorage 合约用作 nonce值的存储。而需要使用 nonce 时,再根据 Create2.computeAddress() 计算得到的地址进行调用,这样就避免了把全局变量存放在 EOA 的 Storage 中。
为了避免采用相同方案的不同协议会 deploy 出相同的 NonceStorage 合约地址,ImplementV1 通过 SALT = keccak256("IMP1") 变量添加了标识特征。即便是后续使用的协议 ImplementV2 采用了和 ImplementV1 相同的 SALT ,也无法重复部署 NonceStorage 合约。
contract ImplementV1 is EIP712 {
bytes32 constant SALT = keccak256("IMP1");
constructor() EIP712("NAME", "VERSION") {}
function initialStorage() public returns (address storageAddress) {
bytes memory bytecode = type(NonceStorage).creationCode;
bytes memory deployCode = abi.encodePacked(bytecode, abi.encode(address(this)));
storageAddress = Create2.deploy(0, SALT, deployCode);
}
function execute(bytes memory signature) public {
address storageAddress = Create2.computeAddress(SALT, keccak256(abi.encodePacked(type(NonceStorage).creationCode, abi.encode(address(this)))));
uint256 nonce = NonceStorage(storageAddress).useNonce();
bytes32 digest = getDigest(nonce);
validateSignature(digest, signature);
}
...
}
NonceStorage 合约的功能就比较简单了:nonce 的储存与修改。
contract NonceStorage is Ownable {
uint256 public nonce;
constructor(address initialOwner) Ownable(initialOwner) {}
function useNonce() public onlyOwner returns (uint256 currentNonce) {
currentNonce = nonce;
nonce++;
}
}
当用户从 ImplementV1 切换到 ImplementV2 时,将会新部署一个属于 ImplementV2 的 NonceStorage 合约来存储 nonce 值。
contract ImplementV2 is EIP712 {
bytes32 constant SALT = keccak256("IMP2");
constructor() EIP712("NAME", "VERSION") {}
... // Same as ImplementV1
}
两个 NonceStorage 合约互相独立,分别存储各自对应协议所使用的 nonce。

通过以上的方案,解决了以下问题:
- 防止签名在同一个 implement 合约上发生重放。
- 解决了 EOA Storage 的冲突问题,使得每个协议拥有了各自独立的内存空间。
Stateless 7702 上存在的签名重放问题
设想这样一个场景,当用户更换 implement 合约时,旧合约与新合约采用的是同一套签名方案,且 _hashedName 和 _hashedVersion 的值也是采用相同的赋值(比如: EIP712("NAME", "VERSION") ),会出现什么情况?
在 oldSimpleImp 上使用过的签名,可以在 newSimpleImp 上进行重放。

我们通过分析 712 的签名内容可以得知,在 7702 的场景下,无论是 oldSimpleImp 上使用的签名,还是 newSimpleImp 上使用的签名,他们的 address(this) 都是一样的,都是 EOA 的地址。所以能够进行重放操作。
function _buildDomainSeparator() private view returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
}
看到这里读者可能会有疑问,为什么要假设两个合约间的 _hashedName 和 _hashedVersion 值是相等的呢?这两个值不相等的话,那不就避免了这个重放的问题了?
是的,如果这两个值不相等,就能避免签名重放的问题。但是问题的重点是开发者与用户无法在协议级别对签名的使用范围进行限制,只能寄希望于使用流程上“采用不同的值”来规避签名重放的风险。
换句话说,开发者在设置 _hashedName 和 _hashedVersion 时无法保证两件事情:
- 当开发者要使用某一套参数时,他需要保证这套参数没有从来没有被使用过。
- 当开发者使用了某一套参数后,他无法保证后来的开发者能够发现这套参数已经被使用,且自觉地避免使用相同的值。
基于以上情况,我们做出了参数值相等这个看似"苛刻"的假设,并且基于这个假设的场景来讨论可能发生的安全问题。
为什么这个问题在 7702 以外的场景(如 4337)没有出现,因为即使他们的 _hashedName 和 _hashedVersion 值相等,address(this) 参数也不相等,所以签名无法跨协议进行重放。

目前 7702 尚未在主网部署,对于可能存在的安全场景还需要在使用过程中发现与挖掘。对于 stateless 7702 授权签名重放的问题笔者暂时没有一个较为完备的解决方法。下面是一些不成熟的想法:
根据 7702 的特性提出更为完备的签名方案,比如在 implement 合约中添加地址,在签名方案中把该地址添加进去。
contract ImplementWithAddress {
address immutable public IMP_ADDRESS;
constructor() {
IMP_ADDRESS = address(this);
}
}
制定 EOA Storage 的使用标准,各个协议间遵循这套标准来使用 Storage,从而避免 Storage 冲突问题。
本文所提到的代码以及测试案例可以在此处访问:https://github.com/ACaiSec/EIP7702SignatureReplay
后记
在写这篇文章的过程中其实是信心不足的,因为 7702 提案还没有真正在主链部署(已经上线了测试链和 Foundry)。所以对于这个提案下的安全场景讨论会略显“纸上谈兵”,担心写出来的内容会缺乏说服力,让读者感觉在“装模作样地吹牛”。但是最终经过多番的修改与调整,还是把整个流程写下来了。希望这篇文章能够给你带来一些收获。如果你读完整篇文章后感觉毫无营养,那真的不是我在敷衍了事,而是哥们的能力就到这里了 T T。如果文章中有什么理解错误的地方欢迎随时指出,有什么想要聊的也可以随时讨论。
【技术分析】EIP-7702 场景下 EOA 授权签名的安全探讨的更多相关文章
- Fabric 和 Sawtooth 技术分析(下)
http://blog.talkingdata.com/?p=6172 在前一篇文章(Fabric和Sawtooth技术分析(上))中,我们着重跟大家分享了 Fabric 相关的内容,在本篇文章中,我 ...
- 硬核测试:Pulsar 与 Kafka 在金融场景下的性能分析
背景 Apache Pulsar 是下一代分布式消息流平台,采用计算存储分层架构,具备多租户.高一致.高性能.百万 topic.数据平滑迁移等诸多优势.越来越多的企业正在使用 Pulsar 或者尝试将 ...
- Linux内存技术分析(下)
Linux内存技术分析(下) 五.内存使用场景 out of memory 的时代过去了吗?no,内存再充足也不可任性使用. 1.内存的使用场景 page 管理 slab(kmalloc.内存池) 用 ...
- 域控场景下windows安全日志的分析--审计认证行为和命令的历史记录
https://www.cnblogs.com/KevinGeorge/p/8563458.html 一.域控windows安全日志基本操作 1.打开powershell或者cmd 1 #gpedit ...
- 网页视频直播、微信视频直播技术解决方案:EasyNVR与EasyDSS流媒体服务器组合之区分不同场景下的直播接入需求
背景分析 熟悉EasyNVR产品的朋友们都知道,EasyNVR不仅可以独成体系,而且还可以跟其他系列产品相配合,形成各种不同类型的解决方案,满足各种不同应用场景的实际需求.针对很多设备现场没有固定公网 ...
- 网页直播、微信直播技术解决方案:EasyNVR与EasyDSS流媒体服务器组合之区分不同场景下的easynvr
近期遇到好多客户咨询关于实现微信直播.或者是将直播页面集成进入自己项目中. 该方案的主要目的:完成在公网一直进行内网摄像头的RTMP/HLS直播! 实现方案的具体实现: EasyNVR+EasyDSS ...
- MySQL金融应用场景下跨数据中心的MGR架构方案(2)
GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 如何在多个数据中心部署多套MGR集群,并实现故障快速切换. 上篇文章介绍了如何在多数据中心部署多套MGR集群,并构建集群间 ...
- MySQL金融应用场景下跨数据中心的MGR架构方案(1)
GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 0. 内容提纲 运行环境 部署MGR A&B 部署MGR A.B之间的复制通道 几个注意事项 如何在多个数据中心部 ...
- SD-WAN技术分析
1.概述 转载须注明来自 SDNLAB并附上本文链接. 本文链接:http://www.sdnlab.com/17810.html 宽带接入以及Internet骨干网容量的持续提升,促使企业WAN技术 ...
- 技术分析:Femtocell家庭基站通信截获、伪造任意短信
阿里移动安全团队与中国泰尔实验室无线技术部的通信专家们一起,联合对国内运营商某型Femtocell基站进行了安全分析,发现多枚重大漏洞,可导致用户的短信.通话.数据流量被窃听.恶意攻击者可以在免费申领 ...
随机推荐
- 计算机视觉中由基本矩阵F或者本质矩阵E计算摄像机投影矩阵时,经常提到“相差一个尺度因子”的含义
在通过二维像素坐标恢复三维坐标的过程中,经常出现这个齐次坐标系.尺度不变性的概念.这篇博客讲的比较好. 一.关于齐次坐标系的直观感受 在我们的世界里,两平行线是永远不会相交的,但是在投影空间里,两条平 ...
- OpenMMLab AI实战营 第三课笔记
OpenMMLab AI实战营 第三课笔记 目录 OpenMMLab AI实战营 第三课笔记 进入 mmclassification 目录 导入工具包 下载数据集 数据集目录结构 下载 config ...
- 将 EasySQLite 从 .NET 8 升级到 .NET 9
前言 EasySQLite是一个.NET 8操作SQLite入门到实战的详细教程,主要是对学校班级,学生信息进行管理维护.今天咱们的主要内容是将EasySQLite从.NET 8升级到.NET 9. ...
- 《CUDA编程:基础与实践》读书笔记(3):同步、协作组、原子函数
1. 单指令多线程模式 从硬件上看,一个GPU被分为若干个SM.线程块在执行时将被分配到还没完全占满的SM中,一个线程块不会被分配到不同的SM中,一个SM可以有一个或多个线程块.不同线程块之间可以并发 ...
- mysql数据库主从同步I/O问题修复
mysql数据库主从同步I/O问题,下面介绍比较靠谱的修复方法. 主节点IP:10.99.202.25,从节点IP:10.99.202.26,修复步骤如下: 1,查看主库repl账号访问权限 mysq ...
- ed25519加密签名算法及应用
刷知乎时看到一篇文章,很感兴趣,来学习一下! 转载文章:ed25519加密签名算法及应用 初次使用Github时都需上传本地的公钥,这时需要提前在本地生成密钥对,使用的是ssh-keygen命令: s ...
- C# Dev GridView当前行
DEV获取GridControl当前行 //直接通过gridView获取当前行 dr=this.gridView1.GetDataRow(this.gridView1.FocusedRowHandle ...
- Oracle连接 ORA-28001: 口令已经失效解决方法
cmd进入命令行 C:\Users\Administrator>sqlplus / as sysdba SQL*Plus: Release 11.2.0.1.0 Production on 星期 ...
- Qt Quick 实现一个右下角弹出消息的组件
目录 开发环境 简介 预览图 如何使用 代码 main.qml MessageView.qml Background.qml ScroolBar.qml MessageQueueView.qml 开发 ...
- 一种基于alpine、支持ARM架构64位的镜像构建方法及其构建系统
本文分享自天翼云开发者社区<一种基于alpine.支持ARM架构64位的镜像构建方法及其构建系统>,作者:郑****团 一种基于alpine.支持ARM架构64位的镜像构建方法及其构建系统 ...