当前位置: 首页 > news >正文

用你自己的签名,打你自己

简介

核心要点:

  • 2026年4月19日至4月26日期间检测到八起攻击事件,覆盖 Ethereum、Avalanche、Sui、Base、HyperLiquid 和 MegaETH 等多条链,总估计损失约 $7.04M。

  • 攻击向量包括 EIP-712 签名覆盖不完整、operator 与 admin 私钥泄漏、跨链定价与跨池奖励 index 的业务逻辑缺陷、预言机配置错误,以及特权执行上下文中的任意外部调用。

  • GiddyDefi 事件($1.3M)表明 EIP-712 签名只保护它哈希过的字段:当 aggregator、fromToken 和 amount 都不在被签名的 payload 中时,任何历史签名都可以被重放,只需把这几个字段换成攻击者的合约。

在过去一周(2026/04/19 - 2026/04/26),BlockSec 检测并分析了八起攻击事件,总估计损失约 $7.04M。下表总结了这些事件,后续小节将对每起事件进行详细分析。

表 1:本周检测到的八起攻击事件概览

本周看点:GiddyDefi

攻击者并没有破解签名,没有借助闪电贷,也没有操纵任何价格。他只是重放了一个合法签名,把签名未覆盖的字段换成了自己的合约。“用你自己的签名打你自己” 是 EIP-712 部分覆盖如何把合法签名变成通用授权这一现象最干净的演示。

2026年4月23日,部署在以太坊上的 GiddyVaultV3 被攻击,损失约 $1.3M。该签名机制只覆盖了 SwapInfo.data,把 aggregator、fromToken、toToken 和 amount 都留在 EIP-712 哈希之外,因此合法签名可以被重放并篡改这些字段。攻击者把 aggregator 指向恶意合约,把 fromToken 指向策略合约持有的 LP Token,从中盗走约 $1.3M。

背景

GiddyVaultV3(0x5f0a…4318)是一个收益耕作金库合约,用户通过 deposit() 和 withdraw() 进行存取。每次操作都必须携带一个由后端签名的 VaultAuth 授权结构体,其中包含一个 EIP-712 签名以及一个描述代币交换路由的 SwapInfo[] 数组。执行交换时,合约调用 GiddyLibraryV3.executeSwap(),对 swap.fromToken 执行 forceApprove,将额度授予 swap.aggregator,然后通过 aggregator.call(swap.data) 执行交换。策略合约随后按其配置策略管理资金。

EIP-712 是一种用于签名结构化链下数据的标准:消费签名的协议在链上重新构造同一个结构体,在约定的 domain separator 下哈希后,恢复签名者地址。任何 EIP-712 流程的安全性因此都取决于链上哈希是否覆盖每一个会影响执行的字段。在 Giddy 的设计中,后端对一个 VaultAuth 进行签名,其中既包含用户意图,也包含任何所需交换的路由指令;_validateAuthorization() 在策略被允许调拨资金之前,会重新构造该结构体来验证签名。

漏洞分析

漏洞位于 GiddyVaultV3 的 _validateAuthorization() 函数中。在构造被签名的 payload 时,每个 SwapInfo 中只有 data 字段被纳入哈希;aggregator、fromToken、toToken 和 amount 全部被排除在签名之外。这意味着任何持有合法签名的人,都可以在仍然通过签名验证的前提下,自由替换 SwapInfo 的其余字段。

四个被排除的字段都是签名未约束的杠杆:aggregator 经 forceApprove 与 aggregator.call(swap.data) 同时成为 spender 与调用目标;fromToken 决定哪种策略资产被授权出去;amount 是授权额度上限;toToken 只用于一次 returnAmount > 0 检查。被签名的 data 并不约束上述任何一项,因为这些目标都不在它的内部出现。

图:_validateAuthorization 中 SwapInfo 签名覆盖不完整

攻击分析

以下分析基于交易 0x5edb66…5482e5。

**Step 1:**攻击者从链上获取了一个由后端合法授权的 VaultAuth 签名,保留 data 字段不变。由于此前每一次 deposit() 或 withdraw() 调用都会把完整的 VaultAuth payload(包括签名)广播到链上,任何历史交易都是免费的可重用签名来源;攻击者只需要找到一个 data 字段适合预期 swap 调用的签名即可。

**Step 2:**攻击者使用拿到的签名,保持 signature、nonce 和 data 不变,篡改其余字段。fromToken 被设为策略合约持有的 LP Token(一种真实资产),让 forceApprove 把协议实际持有的代币授权了出去;aggregator 被替换为攻击者的恶意合约,让授权和后续的 aggregator.call() 都指向攻击者控制的代码。由于这些字段不在签名验证范围内,_validateAuthorization() 接受了被篡改的结构体。为了绕过最后的 require(returnAmount > 0, “SWAP_NO_TOKENS_RECEIVED”) 检查,恶意 aggregator 实现了一个 mint 函数,向协议铸出假代币来满足检查,而无须真的执行任何 swap。

图:被篡改的 SwapInfo 结构体(aggregator 与 fromToken 由攻击者控制)

图:恶意 aggregator 铸假代币以绕过 swap 后余额检查

**Step 3:**由于恶意 aggregator 已经在 step 2 中获得授权,攻击者调用 transferFrom 将金库的 LP 代币直接转入恶意 aggregator,完成盗取。这一步已完全脱离协议的受保护执行路径;当 executeSwap() 返回时,授权已经写入,调用后余额检查也已经通过,协议没有进一步介入的机会。

图:transferFrom 抽走 LP 代币的 Phalcon trace

结论

本次攻击的根本原因是 EIP-712 签名覆盖不完整。直接决定资金流向的 SwapInfo 核心字段没有被保护,攻击者得以在出示合法签名的同时替换 swap 路由和 aggregator 地址。集成外部 aggregator 的开发者应当:

  • 确保 EIP-712 签名覆盖所有影响执行结果的字段,包括 aggregator、fromToken、toToken 和 amount。

  • 对 aggregator 实施白名单,避免调用未审计的外部合约。

  • 将 toToken 限制为预期的基础代币,防止假代币绕过余额检查。

更宏观地看,任何 “先授权后调用” 的架构使用 EIP-712 时,都必须把每一个会影响最终链上状态的字段纳入哈希,而不是仅仅签下用户意图。当后端签名是用户提交参数与协议特权操作之间唯一的把关者时,所有流入该操作的参数(调用目标、资产、金额、接收方)都必须包含在被签名的结构体内。把 data 当作 “调用身份” 的代理是范畴错误:调用的身份是它所有参数的元组,任何被排除在签名之外的参数,按定义就是由提交交易的人所控制的。

本周其它事件

Custom Rebalancer Contract

2026年4月19日,Avalanche 上的一个 sAVAX rebalancer 合约被利用,从一位用户的 Aave V3 信用委托中借出约 $64K(约 7,000 WAVAX)。该合约的一个 public function 在仍持有该用户信用委托的上下文中执行了一个任意的 target.call(data),使攻击者可以调用 Aave 的 borrow() 并把 onBehalfOf 指向受害者。一个白帽机器人抢跑了该攻击,资金在攻击者提款前已被收回。

背景

rebalancer 合约(0x7a7b…a8c9)暴露了一个名为 b2a13230() 的函数,用于重新平衡用户在 Aave 上的杠杆头寸。该函数通过 Aave V3 的信用委托代表用户操作:用户授予 rebalancer 代为借款的权限,rebalancer 把借入的资产与用户提供的资金组合起来调整头寸(例如借+供工作流)。

漏洞分析

根本原因是 b2a13230() 中包含一个 target.call(data) 步骤,其 target 和 calldata 都完全由调用者控制。该 call 运行在合约仍持有用户 Aave V3 信用委托的上下文中,因此该步骤中调用的任何逻辑都继承了用户的借款能力。target 没有任何允许列表,calldata 也没有形状约束,因此该 call 可以调用任何合约方法,包括 Aave 的 borrow() 并把 onBehalfOf 设为用户。

图:b2a13230 函数中的任意 external call

攻击分析

以下分析基于交易 0xaaa1b2…35001b。

**Step 1:**攻击者执行了一笔 sAVAX 与 USDC 的闪电贷。然后通过 rebalancer 合约把借入的 USDC 供给到 Aave V3,建立足够的抵押以便后续借款;与此同时,借入的 sAVAX 被直接转入 rebalancer 合约,为之后的供给步骤做准备。

**Step 2:**攻击者调用 b2a13230() 函数。函数先执行了一次正常的借款,然后到达任意调用部分。此时攻击者构造调用,直接调用 Aave V3 的 borrow(),并把 onBehalfOf 设为受害者地址。由于受害者已对 rebalancer 合约授予信用委托,借款成功,借出的 WAVAX 被转入 rebalancer 合约。

图:通过任意调用代受害者借款的 Phalcon trace

**Step 3:**攻击者再次调用 b2a13230(),这次让 rebalancer 替自己借出 WAVAX。合约随即用前一步借入的 WAVAX(来自受害者头寸的资金)供给到攻击者头寸并偿还,让攻击者得以提取利润。

结论

缺陷在于:一个任意的 external call 处于持有信用委托的特权上下文中。两层只要去掉一层都是安全的:受约束的 external call 无法滥用信用委托,没有信用委托的任意 call 也无法动用用户的资金。持有信用委托的合约不应暴露任意 external call;若必须使用 external call,target 必须固定到允许列表,calldata 必须做形状校验。

REVLoans (Juicebox)

2026年4月20日,REVLoans,一个建立在 Juicebox 之上的借款扩展,在以太坊上被攻击,损失约 $50.7K。borrowFrom() 在未验证调用者提供的会计来源是否已在协议中注册的情况下直接接受;一个伪造的 36 位小数会计上下文触发了同币种快捷路径,将余额错误放大了 1e18 倍。两笔交易,第一笔植入虚增的会计记录,第二笔以膨胀后的份额价格从合法资金池借款,合计拉走 21.77 ETH。

背景

Juicebox 是以太坊上的混合型筹款与借贷协议。每个项目都有自己的 ERC20 份额代币(这里称为 REV),其国库分散在一个或多个 terminal 上:terminal 是负责实际托管项目部分资产的合约,也是用户的入口/出口。一个项目可以在 JBDirectory 中注册多个 terminal,每个 (terminal, project, token) 三元组都附带一个 JBAccountingContext,声明该代币在该 terminal 内部记账时使用的 (decimals, currency)。REV 因此是对该项目所有 terminal 余量并集的索取权,而不是针对任何单个 terminal 的索取权。

用户可以将一种资产存入某个 terminal,换取新铸造的 REV,或在某个 terminal 用 REV 兑换其余量按比例的份额(受可配置的 cash-out tax 影响,会留下一部分价值给剩余持有者)。REVLoans(0x2db6…1846)是叠加在其上的另一个合约,提供借款功能:用户烧掉 REV 作为抵押,从项目某个 terminal 借出贷款,之后可以偿还以重新铸出抵押。借款金额采用与赎回完全相同的数学公式定价,因此一次借款在经济意义上等同于以同一抵押物 cash out。

REV 的份额价格为 (totalSurplus + totalBorrowed) / (REV.totalSupply + totalCollateral)。把 totalBorrowed 计入分子使借款/还款操作对份额价格保持中性;但这也意味着 totalBorrowed 一旦被虚增,份额价格就会直接上涨,让小额抵押能兑换出不成比例的资产。

漏洞分析

根本原因是 source 参数未经校验。borrowFrom() 函数接受一个调用者提供的 REVLoanSource source(包含 .terminal 和 .token 字段的结构体),不检查该组合是否已为给定的 revnetId 注册。两个字段直接流入 cash-out 数学计算,因此 source.terminal 返回的会计上下文完全由调用者控制。当该上下文的 currency 字段与目标 terminal 的 currency 匹配时,协议走同币种快捷路径,跳过价格预言机,把提供的 decimals 精度和余额数字当作可信数据。

图:borrowFrom 接受未经校验的 REVLoanSource

未经校验的 source 随后由 _addTo() 写入 _loanSourcesOf[revnetId] 与 totalBorrowedFrom[revnetId][source.terminal][source.token],该函数同样不做注册检查。

图:_addTo 把未验证 source 写入 _loanSourcesOf,并增加 totalBorrowedFrom

(source, revnetId) 进入账本后,_borrowableAmountFrom() 是把借款请求换算成可支付金额的函数。它从 _totalBorrowedFrom() 取得 totalBorrowed,构造 surplus = totalSurplus + totalBorrowed,再连同调用者的抵押份数与份额总供给一起传入 JBCashOuts.cashOutFrom()。

图:_borrowableAmountFrom 拼装 surplus = totalSurplus + totalBorrowed 并传入 cashOutFrom

decimals 精度 bug 藏在更深一层的 _totalBorrowedFrom() 里。它遍历 _loanSourcesOf,每条记录通过 mulDiv(tokensLoaned, 10decimals, pricePerUnit) 折叠累加。在同币种路径上,pricePerUnit = 10decimals(目标的 18 位精度),公式退化为 tokensLoaned 不变;以 36 位小数记账的余额落入 18 位小数 ETH 求和时被放大 1e18 倍。

图:_totalBorrowedFrom 同币种快捷路径以目标 decimals 作为 pricePerUnit,导致 1e18 倍错误归一化

放大发生在 cashOutFrom() 内。base = mulDiv(surplus, cashOutCount, totalSupply):当 surplus 被虚增的 totalBorrowed 主导,再小的 cashOutCount(抵押)也会映射成一笔不成比例的支付。

图:cashOutFrom 的 base 公式,展示膨胀的 surplus 如何放大支付

攻击分析

整个攻击使用了两笔交易。第一笔污染 REVLoans 的账本:0xc46cb7…dead1f。第二笔针对合法 terminal 抽干资金池:0x9adbd6…a8f938。

**Step 1:**攻击者调用 borrowFrom(),loan source 中的 terminal 和 token 均指向一个伪造合约,并存入少量 REV 作为抵押。REVLoans 既不检查所提供的 terminal 是否在 revnet 中注册过,也不检查 token 是否被它识别。

图:Tx1 borrowFrom 的 Phalcon trace —— source.terminal 与 source.token 均指向伪造合约 0xbd18…35e2

**Step 2:**REVLoans 向伪造 terminal 询问会计上下文,得到伪造的 (decimals=36, currency=ETH-code(61166))。由于源币种与目标币种相匹配,REVLoans 走同币种快捷路径并跳过价格预言机,将合法 terminal 的真实 ETH 余量按攻击者的 36 位小数目标单位重新表达,使数值被放大 1e18 倍。

图:伪造 terminal 上的 accountingContextForTokenOf 返回 (decimals=36, currency=61166)

**Step 3:**REVLoans 把 (fake terminal, fake token) 注册进 _loanSourcesOf,并把被夸大的数值写入 totalBorrowedFrom。伪造 terminal 仅以 “确认收到” 的方式 “放款”;没有任何真实 ETH 移动。第一笔交易在 totalBorrowed 被向上操纵且仅烧掉少量 REV 抵押的状态下结束。

图:_addTo 中的运行时状态 —— REVLoanSource 推入 _loanSourcesOf,totalBorrowedFrom 增加 36 位小数尺度的金额(约 2.29e28)

**Step 4:**攻击者再次调用 borrowFrom(),这次把合法的 ETH terminal 作为 loan source,仅以极小数量的 REV 作为抵押。cash-out 的数学计算这一次以真实的 18 位小数 ETH 单位运行。

图:Tx2 borrowFrom 的 Phalcon trace —— source.terminal=JBMultiTerminal,currentSurplusOf 在 decimals=18 下运行

**Step 5:**在计算 totalBorrowed 时,REVLoans 遍历 _loanSourcesOf 并触及 step 3 中的记录。由于该记录的 currency 仍与 ETH 匹配,同币种快捷路径再次触发,把以 36 位小数存储的余额折叠进 18 位小数的 ETH 求和中时放大了 1e18 倍。totalBorrowed 此时被假债务主导,份额价格分子被极大膨胀。

图:运行时 _totalBorrowedFrom mulDiv 处理被污染的条目 —— x=4.45e29 (36-decimal), y=1e18, denominator=1e18

**Step 6:**cash-out 的数学计算返回了一个由膨胀后的分子决定的借款金额,该金额已被攻击者预先调整为略低于合法 terminal 的真实余量。合法 terminal 把这笔款付了出去,几乎掏空了整个资金池。

图:运行时 cashOutFrom 返回约 23.15 ETH(23,154,329,666,570,993,199 wei),由膨胀的 totalBorrowed 推导而来

结论

根本原因是两个复合缺口:(terminal, token) 未做 revnet 注册校验,且同币种快捷路径在折叠余额时没有对 decimals 差异重新归一化。任何一个缺口单独存在危害都有限;叠加后,调用者可以注入任意 totalBorrowedFrom 记录并按面值兑出。修复:对 (terminal, token) 做 revnet 注册校验,并在折叠前按 source 存储的 decimals 精度重新归一化余额。

Volo Vault

2026年4月22日,Sui 上的 yield vault 协议 Volo(把用户存款路由到借贷协议 Navi 获取收益)在 operator 私钥泄漏后损失约 $3.5M。金库合约本身没有代码层 bug;攻击者只是以被盗凭据走了一遍合法的 operator 路径,把 Volo 在 Navi 上的存款抽干。

背景

Volo 是面向用户的金库(0xcd86…27fefa);Navi 是底层借贷协议。金库持有 Navi AccountCap(Sui 的 capability 对象,授权对 Volo 在 Navi 上账户的提取操作),并把策略调度权委托给 operator 角色。要在 Navi 上存取,operator 调用 start_op_with_bag_v2() 把 AccountCap 从金库提到一个临时 bag 中,再用该 cap 通过 deposit_with_account_cap() / withdraw_with_account_cap_v2() 移动资金。

漏洞分析

根本原因是运营/密钥托管层面的失败,而不是合约层漏洞。Volo 策略路径把提取权限委托给任何持有 operator 私钥的人:start_op_with_bag_v2() 只做两项检查(assert_operator_not_freezed(operation, cap) 和 assert_single_vault_operator_paired(operation, vault.vault_id(), cap)),两者都只验证传入的 capability 是已注册的 operator。withdraw_with_account_cap_v2() 随后接受任何能出示已提取 AccountCap 的调用者。任何持有 operator 私钥的人因此都能执行与合法操作完全相同的路径。

图:start_op_with_bag_v2 仅有的两项检查 —— assert_operator_not_freezed 与 assert_single_vault_operator_paired

攻击分析

以下分析基于交易 AQw9wM…3RUS。

**Step 1:**攻击者以泄漏的 operator 私钥在 @volosui/volo-vault::operation 上调用 start_op_with_bag_v2,把 Navi AccountCap 提到临时 bag 中。

图:在 @volosui/volo-vault::operation 上对 start_op_with_bag_v2 的 Move 调用

**Step 2:**攻击者用 bag::remove 从临时 bag 中取出 AccountCap。

**Step 3:**攻击者用取出的 AccountCap 在 @navi-protocol/lending::incentive_v3 上调用 withdraw_with_account_cap_v2,把 Volo 的存款从 Navi 提取出来。

图:用提取出的 AccountCap 在 @navi-protocol/lending::incentive_v3 上调用 withdraw_with_account_cap_v2

Step 4:攻击者用 bag::add 把 AccountCap 放回,关闭操作并把资金转出。

结论

缺陷是结构性的:一把 operator 密钥、完整提取权限、没有第二道检查。三种改动可以降低密钥泄漏的破坏。把 operator 角色拆到多签或门限方案,泄漏的单一密钥无法独立授权提取。为对外提取加时间锁,异常调用在结算前会留出可质疑的窗口。把 operator 的权限收窄到仅可存入与再平衡,用户面提取走另一条独立路径,泄漏的 operator 密钥就触及不到用户资金。

Kipseli Router

2026年4月22日,Base 上的 Kipseli Router 被攻击,损失约 $72.35K。该 router 把外部 USDC-only quoter 返回的值直接当作输出代币的原始转账金额,不校验输出代币与报价代币是否一致。攻击者在 quoter 实际不支持的路径上把 0.04 WETH 换成 cbBTC,把 quoter 返回的 USDC 量级数值(92,610,395)当作 cbBTC 原始单位拿到(约 0.926 cbBTC)。

背景

Kipseli Router(0x579f…9a07)是一个由外部报价系统支撑的 swap 执行合约。该合约未开源,下面的分析基于反编译字节码,因此函数名以 4-byte selector 形式出现(0xcce096f3()、0x592()、0xd88())。它不直接从链上 AMM 池计算 swap 价格,而是向 quoter 询问输出金额(amountOut),然后基于该值执行代币转账。正常操作下,用户把 tokenIn 发送到协议钱包,router 从同一钱包把 tokenOut 拉出转发给接收者。该协议配置了单一 QUOTE_TOKEN,报价逻辑以 USDC(精度为 6)记账;整个系统只支持 USDC 计价的报价。

漏洞分析

缺陷分布在两层并相互叠加。router 侧:函数 0xcce096f3() 通过 quoter 函数 0x592() 获取报价 v0,原样传入 0xd88() 作为 tokenOut.transferFrom(_wallet, receiver, v0)。router 不检查 tokenOut 是否等于协议的 QUOTE_TOKEN,因此一个 USDC 计价的数值(精度为 6)被当作 cbBTC 数量(精度为 8)转出。Quoter 侧:底层 PropAMM AMM 仅为 token-to-USDC 对设计,但接受未支持的路由路径(WETH → cbBTC)而不 revert,静默忽略 tokenIn 并返回一个 USDC 量级的值,仿佛该 swap 合法。

图:反编译的 0xcce096f3 —— v0 = 0x592(…) 获取报价后,0xd88(v0, …) 用 v0 作为 transferFrom 的原始金额

攻击分析

以下分析基于交易 0x96edee…3db3bb。

St****ep 1:攻击者向 router 发起 tokenIn=WETH、tokenOut=cbBTC 的调用。底层 AMM 不支持该路径但没有 revert,quoter 0x592() 返回了 USDC 量级的 92,610,395(约 92.61 USDC)。

图:router 调用 quoter 询问 WETH→cbBTC 路径的 Phalcon trace —— staticcall 返回 USDC 量级的值

Step 2:router 把该值直接当作 cbBTC 的转账金额。0.04 WETH(约 $95)通过 transferFrom 流入;92,610,395 原始 cbBTC 单位(约 0.926 cbBTC,约 $72.35K)从协议钱包流向攻击者。

图:完整攻击的 Phalcon trace —— 0.04 WETH 转入,92,610,395 cbBTC 原始单位转给攻击者

结论

漏洞之所以成立,是因为 quoter 调用的两侧都未检查各自的假设:quoter 假设它的输出会在自己的 USDC 6 位精度框架里使用;router 假设 quoter 返回的值就是请求中 tokenOut 的数量。任意一边补上检查都能消除该缺陷:

  • router 侧:断言 tokenOut == QUOTE_TOKEN,或在转账前通过预言机把 USDC 量级的报价换算成 tokenOut 单位。

  • Quoter 侧:对未注册在支持代币对集合内的路由路径直接 revert,而不是静默返回一个 USDC 量级的兜底值。

Purrlend

2026年4月25日,HyperLiquid 与 MegaETH 上的借贷协议 Purrlend 因私钥泄漏损失约 $1.5M。攻击者接管 bridge 角色后铸出无底层资产支撑的 pTokens(Purrlend 类 Aave 的存款凭证),再以这些 pTokens 作为抵押借出资金池里的真实资产。

背景

Purrlend(0x81d5…a702)是一个采用类 Aave 会计模型的借贷协议。当用户向协议供给资产时,会获得相应的 pTokens,类似于 Aave 的 aTokens,代表其供给头寸,可作为借出其他资产的抵押。

协议还包括 pool admin、risk admin 和 bridge 等特权角色。bridge 角色用于跨链记账:可以铸出 pTokens 以镜像在对端链上发生的存款。其他 admin 角色用于修改风险参数和配置可借资产。

漏洞分析

直接触发是特权私钥泄漏:攻击者获得了控制 Purrlend admin 和 bridge 角色的密钥。合约层一处设计缺陷把这次密钥泄漏放大了:bridge 角色的 pToken 铸造路径没有锚定任何可验证的跨链托管证据。该函数允许持有 bridge 角色的调用者把 pTokens 铸到任意地址、任意数量,不检查源链是否真的发生了对应存款。在协议的其他位置,pTokens 一律按合法抵押处理,借款路径也不会在借款时重新核验底层支撑。因此一次未授权的 bridge 铸造直接转化为借款能力,从铸造到提款之间没有第二道关卡。

攻击分析

以下分析基于 MegaETH 上的交易 0xb96cff…dbbf24。

Step 1:攻击者持有泄漏的特权私钥,通过 GnosisSafeProxy 提交一个 MultiSendCallOnly 批量调用,把自己经 ACLManager 设为 pool admin、risk admin、bridge 和 emergency admin,随后将 WETH 设为可借资产并把其 BorrowCap 配置为 200。

图:GnosisSafeProxy 上 multiSend 授予 ACLManager 角色并配置 WETH 借款的 Phalcon trace

Step 2:攻击者以 bridge 身份把大量 pTokens 铸到自己的地址。该铸造路径不做任何跨链托管核验,新铸的 pTokens 没有任何底层资产支撑。

Ste****p 3:攻击者用这些无底层支撑的 pTokens 作为抵押。由于借款路径把任意 pToken 余额都当作合法供给头寸,不会重新核验底层,抵押检查通过,WETH 被借出资金池。

结论

这是一次被合约层设计缺陷放大的私钥泄漏。被盗的密钥本身只赋予攻击者 bridge 角色的设计授权,但这个授权包含了对 pToken 不受约束的铸造能力,直接转化成可借出的抵押。两层都可以独立收紧。运营层:把 bridge 角色拆到多签或门限方案,单一密钥泄漏无法独立调用。合约层:在铸造时要求传入可验证的跨链托管证明(例如来自可信跨链验证器的消息承诺),无证明即 revert。在铸造时验证证明是更彻底的修复,因为它把对密钥托管的依赖整个去掉。

SingularityFinance

2026年4月26日,Base 上的 SingularityFinance dynBaseUSDCv3 金库损失约 $413K。该金库被配置成使用一个不存在于 Uniswap V3 的 fee tier(42),导致每一个非 USDC 资产的价格预言机解析到不存在的池子。定价函数没有 revert 而是静默返回 0,金库把非 USDC 储备估为零,攻击者只用极少量 USDC 存入就铸出几乎全部份额,再赎回换取真实底层资产。

背景

dynBaseUSDCv3 金库(0x67b9…4dcd)持有多种生息代币,通过 Uniswap V3 给非 USDC 储备定价。定价辅助函数 getPrice(base, fee, quote, amount) 先用 factory 把 (base, quote, fee) 三元组解析成 Uniswap V3 池,再从该池读 TWAP。金库的 totalAssets() 对这些定价后的储备求和;份额铸造与赎回比率从这一总值推导。

漏洞分析

缺陷在 getPrice() 的提前返回分支。当 IUniswapV3Factory.getPool(base, quote, fee) 返回 address(0)(该 fee tier 没有对应池)时,函数没有 revert 而是直接返回零初始化的 price 变量。该金库部署时配置 fee=42,不属于 Uniswap V3 的支持档位(500/3000/10000),因此每一个非 USDC 代币的查询都落入这个分支。totalAssets() 实际上只剩金库的 USDC 余额,生息代币贡献为 0。所有依赖 totalAssets() 的铸造和赎回比率,都在这个接近零的分母上计算。

图:getPrice 在 getPool 返回 address(0) 时返回 0 而非 revert

攻击分析

以下分析基于交易 0x00b949…8d3732。

Step 1:攻击者闪电贷借入约 100K USDC。

Step 2:攻击者把 USDC 存入金库。由于 totalAssets() 只算 USDC 余额,金库的自我估值约等于这次存入额,攻击者按这个比例拿到了几乎全部份额。

Step 3:攻击者赎回份额,赎回按持有比例分配底层储备。攻击者从金库持有的每一种生息代币中都拿到了相当大的一部分。

Step 4:攻击者偿还闪电贷,把拿走的生息代币留作利润。

结论

两层校验都缺位。部署时没有按 Uniswap V3 支持的档位(500/3000/10000)校验 fee=42;getPrice() 又在池不存在时返回 0 而非 revert。任意一层补上都足够:在配置时校验预言机参数,或在 getPool() 返回 address(0) 时 revert。作为纵深防御,份额铸造逻辑可在接受存款前把 totalAssets() 与外部参考做一次健全性核对。

Scallop

2026年4月26日,Scallop 在 Sui 上的 staking 奖励程序损失约 $142.7K。领取奖励的路径上有一处校验缺失:更新累积奖励的函数缺少账户与传入对象之间的归属校验,任意奖励跟踪对象都能被接受。攻击者借此从一个被遗弃、长期不活跃的奖励跟踪对象中拉出一笔虚构的 points 余额,再以这些 points 对合法奖励池 1:1 兑出,直到池子余额被掏空。

背景

Scallop 是 Sui 上的借贷协议。在借贷产品之上,Scallop 运行一个 spool 程序:用户把单一资产存入 Scallop 市场获得 MarketCoin(借贷凭证;SUI 存款对应 MarketCoin,即 “sSUI” 在链上的表现形式),把该 MarketCoin 质押到一个 Spool 中随时间积累协议 points,之后再到对应的 RewardsPool 中兑换实际奖励代币。每个 Spool 是一个 Sui 共享对象,跟踪一个全局 per-share index;每个用户持有一个个人 SpoolAccount,记录其质押余额和已累积的 points。

漏洞分析

缺陷在 spool::user::update_points:该函数没有断言 account.spool_id == object::id(spool)(也未断言 account.stake_type == spool.stake_type)。同级条目 stake、unstake、redeem_rewards 都在入口处做了这道绑定检查,唯独 update_points 没有。缺了这道检查,spool_account::accrue_points 就会针对任何传入的 Spool 执行 account.points += stake * (spool.index − account.index) / 1e9,把那个 Spool 的 index 当作本账户的奖励流。

图:update_points 缺少 stake/unstake/new_spool_account 都会做的 assert_spool_id 绑定检查

这条路径之所以可被利用,是因为 Sui 从不回收共享对象:被遗弃的 Scallop Spool,其 stakes 萎缩到极少后仍会持续累计奖励份额(每周期增量为 1e9 * reward / stakes),index 随时间持续累加,可以达到任意大的值。由于 update_points 又缺少绑定校验,它能用这种异常 index 给任意账户写入一大笔 points 增量。这些被污染的 points 随后能在目标 spool 的 RewardsPool 上以 1:1 兑出,因为账户本身合法绑定到目标 spool,redeem_rewards 的绑定校验通过。

攻击分析

以下分析基于交易 6WNDjC…NfVL。

Step 1:攻击者以 0.2 SUI 作为诱饵,铸出 MarketCoin,然后调用 new_spool_account + stake 针对目标 spool 创建一个合法绑定的 SpoolAccount,account.spool_id = target_spool。

Step 2:攻击者调用 update_points<MarketCoin>(donor_spool, account, clock),donor_spool 传入一个被遗弃的 Spool。捐赠者的 index(约 8.91e14)被作为 points 写入账户:points = stake * (8.91e14 − 1.19e9) / 1e9 ≈ 1.62e14。

Step 3:攻击者调用 redeem_rewards<MarketCoin, SUI>(target_spool, target_rp, account)。绑定断言接受了绑定到目标的账户,内部 re-accrue 提前返回,被污染的 points 按奖励池 1:1 的比率兑换至池子余额上限:rewards = 150,098,061,595,978 原始 SUI。

Step 4:攻击者调用 unstake 和 redeem 取回 0.2 SUI 诱饵,再用 TransferObjects 把所有东西转出。

结论

修复方法是在 update_points 入口加上与 stake、unstake、redeem_rewards 一致的 assert!(account.spool_id == object::id(spool)) 检查。作为纵深防御,协议还可以为单次 accrue_points 调用接受的 index 增量设上限(超出阈值即 reject),即使将来绑定检查再次被绕过,单次调用也无法给账户记入与实际 staking 时长不成比例的 points。

http://www.gsyq.cn/news/1595952.html

相关文章:

  • 微信会话存档亿级数据处理:基于 RSA 混合解密与 Flink 的流式架构实战
  • C#工业相机开发从零到一:图像采集与显示的工程化实战
  • 从CTF实战解析SQL注入:绕过过滤与联合查询攻防
  • Python+Selenium自动化测试:Chrome Driver版本管理全流程实现
  • 天行健与优胜劣汰:两种文明范式的哲学比较及其现代启示
  • LSR包胶技术深度解析:金属包胶、塑料包胶到底怎么做?
  • OpenAI 9 个月自研芯片 Jalapeño,推理成本砍半,ChatGPT 体验将大升级!
  • 天河应用大讲堂 | 基于人工智能的天气预报技术发展趋势
  • 打通企微接口,构建适配 GEO 检索规则的结构化素材库
  • 从安装到调优,Strix Halo 本地大模型一周使用实录
  • C++跨平台(一):开发概述与策略选择
  • 合同系统智能化,让企业合同管理快人一步!
  • iOS网络安全实战:AFNetworking证书锁定防御中间人攻击
  • 《赣州市本级政府投资数字化项目费用编制指南》(赣市财审字〔2026〕2号)标准解读
  • 什么是企业号码认证?
  • Gogs高危漏洞实战:从原理到修复的完整安全加固指南
  • 开源编程Agent来了,企业AI选型三大新命题 - 微元算力(weytoken)
  • AI专著写作高效之道:借助AI工具,轻松打造20万字优质专著!
  • QuickQanava 源码阅读笔记(二):edge、容器适配器与 noexcept 的极致
  • 国家社科基金项目申报资料(含申报书范本,立项清单、各阶段报告及申报经验)
  • AI写论文有妙招!4款AI论文生成工具,解决你的写作难题!
  • QMCDecode:macOS上快速解密QQ音乐加密音频的终极指南
  • 山东先进网上阅卷公司有哪些
  • CAD Electrical 2027安装教程(2026年保姆级超详解)【附安装包+电气符号原理图指南】
  • 从Kac-Moody代数到群概形:构造、完备化与仿射型实现
  • 传统食品企业数字化转型案例:河北康贝尔的直播破局之路
  • 大厂Agent架构我拆了三遍,发现一人公司只需要3个文件(附模板)
  • Moto 手机自带天气不会用?桌面插件一键添加城市,不用下载第三方 APP
  • 半年估值暴增2.5倍!Baseten融资15亿美元,成AI推理时代基础设施宠儿
  • Visual C++ Redistributable AIO:一站式解决Windows运行库缺失问题的终极指南