尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

TrustedVolumes 遭黑事件深度解析:670 万美元的 RFQ 授权灾难

TrustedVolumes 遭黑事件深度解析:670 万美元的 RFQ 授权灾难 TrustedVolumes 遭黑事件深度解析670 万美元的 RFQ 授权灾难三个小漏洞如何连锁触发掏空做市商全部库存2026 年 5 月 7 日TrustedVolumes, 一家在 1inch 的 RFQRequest-for-Quote询价生态系统中运作的流动性提供者与解析器Resolver, 其存放在 WETH、WBTC、USDT 及 USDC 的资产约 670 万美元在一夕之间被全数提空。而这一切仅发生在一笔精心策划的单一交易之中攻击者利用了协议自定义 RFQ 代理合约中三个环环相扣的授权失效问题。这起事件最值得深究的地方并非攻击手法有多么精密而是其漏洞本身简直「朴实无华」。这不是什么零日漏洞攻击也不是新颖的密码学破解而是一场典型的存取控制失败、参数混淆与状态管理崩溃的教科书级案例。让我一步步带你剖析到底发生什么事、为什么会发生以及最重要的是如何确保你的合约不会重蹈覆辙。TrustedVolumes 原本应该如何运作TrustedVolumes 作为 1inch Fusion 中 RFQ 系统的做市商与解析器其架构遵循一个非常直观的模式一个库存金库Inventory Vault0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31持有协议的数字货币储备。该金库向 RFQ 代理合约授予了**无限量的 ERC-20 授权Approval**这在做市商领域虽常见但极度危险。RFQ 实作合约Implementation0x88eb28009351Fb414A5746F5d8CA91cdc02760d8包含订单填充的逻辑。RFQ 代理合约Proxy0xeEeEEe53033F7227d488ae83a27Bc9A9D5051756作为公开的入口点。在正常的 RFQ 流程中做市商Maker会事先授权特定的签署者Signer吃单方Taker提交已签章的订单代理合约验证签章是否在允许清单内、检查重放保护Replay Protection然后透过transferFrom从做市商的库存中执行原子交换Atomic Swap。原先应该被保证的三道防线是只有被授权的签署者能批准订单、每个签署过的订单只能被填充一次、代币的来源必须是经过验证的做市商自有库存。很遗憾这三道防线在同一时间全面溃堤。让一切化为泡影的三个漏洞漏洞 1无许可的签署者注册存取控制失效registerAllowedOrderSigner(address signer, bool allowed)函数的设计初衷是让做市商能够注册授权的订单签署者。然而问题在于这个函数被设定为public而且它的存取控制仅仅检查了msg.sender,完全没有检查呼叫者是否真的具备「做市商」权限。// 漏洞代码 function registerAllowedOrderSigner(address signer, bool allowed) public { _allowedSigners[msg.sender][signer] allowed; // 任何人都能为「自己的地址」注册「任何签署者」 }这代表任何一个人, 就是任何路人甲都能呼叫这个函数并将任何 EOA 地址指定为自己的有效签署者。攻击者部署了一个攻击合约随即呼叫registerAllowedOrderSigner(0xC3...9100, true)轻轻松松将自己的 EOA 变成了该攻击合约作为做市商的合法签署人。这就像银行让任何人走进来把自己加到「授权支票签署人」的名单上一样离谱。漏洞 2完全失灵的重放保护状态管理失败协议试图透过salt随机数机制来记录哪些订单已经被填充过。但悲剧的是读取与写入填充状态时所使用的储存位置Storage Slot竟然不一样// 读取填充状态合约检查时 bool filled _filledOrderSalts[order.salt]; // 从某个位置读取 // 写入填充状态合约更新时 _filledOrderSalts[order.someOtherField] true; // 写入了「完全不一样」的位置结果就是同一笔订单可以被无限次提交、无限次填充。重放保护机制看似编译通过实际上完全在空转。每次攻击者提交相同的伪造订单时合约都去检查一个「永远不会被更新」的储存槽然后愉快地判定「这笔订单还没填充过」。这就像门锁检查门有没有锁好时却去盯着隔壁那扇门看。漏洞 3关键的参数错配授权来源失效这是三个漏洞中最致命的一个也是真正导致 670 万美元被盗的元凶。当一笔 RFQ 订单被填充时order.inventory字段. 一个在调用数据Calldata中完全由攻击者控制的地址竟然直接被当作transferFrom的from参数传入。整个验证流程只证明了「攻击者的 EOA 是攻击者控制的接收合约的有效签署者」但它完全没有证明order.inventory与这个签署者有任何关系。// 漏洞代码 function fillOrder(Order memory order, bytes memory signature) public { // 检查签署者是否为 msg.sender吃单方的授权签署者 require(_allowedSigners[msg.sender][recoveredSigner], Not authorized); // 执行从 order.inventory 提取代币完全由攻击者操控 IERC20(token).transferFrom(order.inventory, recipient, amount); }攻击者将inventory字段设为0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31,正是 TrustedVolumes 做市商自己的托管地址而该地址早就对 RFQ 代理授予了「无限代币使用权」。这就像一个系统检查了你的身份证确认你有权进入大楼然后却让你从别人的钱包里拿钱走人只因为你走了不同的门。攻击行动全还原一步一步拆解以下是攻击者实际执行的流程步骤 1部署攻击合约。攻击者部署了辅助合约0xD4D5DB5EC65272B26F756712247281515F211E95。步骤 2将自己注册为授权签署者。辅助合约呼叫registerAllowedOrderSigner(0xC3...9100, true)将攻击者的 EOA 设定为该辅助合约作为做市商的有效签署者。严格来说这在技术上是「合法」的因为攻击者只是在自己的地址下授权自己。步骤 3伪造签署过的订单。攻击者使用已注册的签署者地址签署了一份maker 攻击合约的订单轻松通过签章验证。至于其他参数如代币种类、金额等完全可任意填写。步骤 4利用参数错配发动攻击。在未签署的调用数据中攻击者将from即inventory设为 TrustedVolumes 的解析器地址。由于fillOrder函数从未要求「签署时的 maker」必须等于「执行时的 from 地址」因此攻击者的签章依然有效。步骤 5掏空金库。合约顺利验证了针对攻击者自己地址的签章然后「尽忠职守」地透过先前设立的无限授权从 TrustedVolumes 的库存中把钱转走。步骤 6转换并藏匿赃款。几小时内所有被盗资产都被换成 ETH并分散至多个钱包。其中一个地址0x61e6301614178a2ca21bfa0fbb30aba06acc2d1c收到了约 137 万美元的 WBTC 和 127 万美元的稳定币随后被全数兑换成 1,222.12 颗 ETH。事后发展部分资金返还与混乱的「和解」离奇的是攻击者后来返还了 1,122 颗 ETH 给协议方同时保留了约 200 万美元作为某种「赏金」留存。这种模式: 部分退款、非正式谈判、以赏金形式和解, 在 DeFi 黑客事件中已逐渐成为令人不安的常态。TrustedVolumes 最初曾祭出漏洞赏金并表示愿意「就双方皆能接受的解决方案进行建设性沟通」。部分资金追回总比没有好但这并不能掩盖根本问题这些漏洞当初为什么会存在如何根治这个问题正确的修正方案修正方案 1严格限制特权函数的存取权限问题所在registerAllowedOrderSigner被设为public而它理应受到严格限制。正确写法// 漏洞写法不安全 function registerAllowedOrderSigner(address signer, bool allowed) public { _allowedSigners[msg.sender][signer] allowed; } // 安全写法修正后 mapping(address bool) public isMaker; // 只有协议认可的做市商 function registerAllowedOrderSigner(address signer, bool allowed) external { require(isMaker[msg.sender], Only registered makers can authorize signers); _allowedSigners[msg.sender][signer] allowed; }为什么有效确保只有协议认证过的做市商才能授权签署者彻底封堵自行注册的漏洞。函数的可见性应预设为private或internal除非有强烈的商业逻辑要求它必须公开。修正方案 2将授权与执行环境强制绑定问题所在合约针对msg.sender检查授权却从完全不同的order.inventory提取资金。正确写法// 漏洞写法不安全 function fillOrder(Order memory order, bytes memory signature) public { require(_allowedSigners[msg.sender][signer], Not authorized); IERC20(token).transferFrom(order.inventory, recipient, amount); } // 安全写法修正后 function fillOrder(Order memory order, bytes memory signature) public { // 库存来源「必须」与授权签署者的地址是同一个 require(_allowedSigners[order.inventory][signer], Signer not authorized for this inventory); IERC20(token).transferFrom(order.inventory, recipient, amount); }为什么有效将授权检查与资金来源紧密耦合。传入transferFrom的规范做市商地址必须与权限映射中验证的地址完全一致。只要「被检查授权的地址」与「被扣款的地址」出现任何偏差都是致命的漏洞。修正方案 3正确实作重放保护机制问题所在合约读取与写入填充状态时使用了不同的储存位置。正确写法// 漏洞写法不安全 mapping(bytes32 bool) private _filledOrderSalts; function fillOrder(Order memory order, bytes memory signature) public { require(!_filledOrderSalts[order.salt], Order already filled); // ... 执行转账 ... _filledOrderSalts[order.someOtherField] true; // 写错位置了 } // 安全写法修正后 mapping(bytes32 bool) private _filledOrderSalts; function fillOrder(Order memory order, bytes memory signature) public { require(!_filledOrderSalts[order.salt], Order already filled); // ... 执行转账 ... _filledOrderSalts[order.salt] true; // 正确读写使用同一个 Key }为什么有效确保检查与标记填充状态使用同一个储存键值Key让重放保护确实发挥作用。修正方案 4限制授权额度的曝险范围问题所在金库直接授予 RFQ 代理无限额度的代币使用权。正确写法// 漏洞写法不安全 IERC20(token).approve(proxyAddress, type(uint256).max); // 无上限 // 安全写法修正后 // 使用单笔订单专属的授权并设定有效期限 IERC20(token).approve(proxyAddress, orderAmount buffer); // 或者采用「拉取Pull」机制每笔订单都需独立明确授权为什么有效每笔订单或每项资产独立设定授权上限并加上时效限制能戏剧性地缩小单一漏洞被利用时的爆炸半径。高价值做市商对代理合约授予无限授权就像把火药库的钥匙挂在门口。修正方案 5拒绝语义模糊的订单结构问题所在订单中的maker、taker、receiver字段可以各自为政且无需透过单一签章对所有字段做出承诺。正确写法// 漏洞写法不安全 function fillOrder(Order memory order, bytes memory signature) public { // 只检查签章有效性未检查上下文一致性 } // 安全写法修正后 function fillOrder(Order memory order, bytes memory signature) public { // 签章必须「承诺」所有关键字段 bytes32 orderHash keccak256(abi.encode( order.maker, order.taker, order.inventory, order.token, order.amount, order.salt )); require(recoverSigner(orderHash, signature) order.maker, Invalid signature); // 接着验证执行上下文是否与签署时的意图相符 require(order.inventory order.maker, Inventory must match maker); }为什么有效将所有相关地址绑定在同一个签署过的意图中。如果订单的字段可以未经完整签章承诺而各自偏离那么合约就是运行在一个残缺的信任模型之上。更深层的教训这一切原本都可以避免TrustedVolumes 遭黑事件一点也不「高深」。它是基础安全措施失灵的结果任何合格的审计公司都应该能轻易抓出这些问题。事实上在事发之前市场上并未有任何公开的智能合约审计报告, 而这种基础的存取控制缺陷任何标准审计流程都不该放过。更令人遗憾的是同一个攻击者曾在 2025 年 3 月利用 1inch Fusion V1 的漏洞得手约 500 万美元。TrustedVolumes 显然未将该次业界震撼教训纳入自家系统的防护考量。身为开发者我们必须将以下几点刻进骨子里存取控制绝非选配功能。将函数标记为public虽然简化了权限管理因为不用担心授权使用者无法呼叫但这可能彻底摧毁安全防线。请预设使用private或internal除非有强烈且明确的业务需求。参数混淆是沉默的杀手。在 RFQ 模型下做市商授予授权的前提是「只有通过验证的交易对手方才能触发订单填充」。当合约对着 A 字段检查权限却对着 B 字段执行扣款时这个核心假设便彻底崩溃。代理合约是巨大的攻击面。未经严格审查的代理合约背后扛着高价值的做市商授权这是一种结构性的风险。请用与核心协议同等严格的标准来审计代理合约。无限授权极度危险。代理合约被攻破时造成的损失与它持有的授权额度成正比。请务必设定上限没有第二句话。总结TrustedVolumes 遭黑事件是一记沉重的警钟它提醒我们在 DeFi 的世界里最微小的实作瑕疵都可能引发灾难性的损失。三个漏洞, 每一个单独来看都极度简单串联在一起就能从一个承载真实经济活动的协议中卷走数百万美元。部分资金的追回并未改变核心现实代码风险从来都不是抽象概念。即便是活跃运作中的协议也可能因为一个小实作缺陷而酿成大祸。对开发者而言教训更加锋利签章验证、存取控制、代理逻辑与升级路径都需要以最严苛的标准进行审查因为攻击者只需要找到「一个」弱点。审计你的合约。测试所有的边界案例。反复挑战你自己对「什么被授权、什么不被授权」的假设。永远、永远不要因为代码能编译通过就想当然地认为它万无一失。攻击交易哈希0xc5c61b3ac39d854773b9dc34bd0cdbc8b5bbf75f18551802a0b5881fcb990513漏洞合约地址0x88eb28009351Fb414A5746F5d8CA91cdc02760d8受害者合约解析器0x9bA0CF1588E1DFA905eC948F7FE5104dD40EDa31
返回列表