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

📅 2026/6/26 22:05:10
用你自己的签名,打你自己
简介核心要点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/26BlockSec 检测并分析了八起攻击事件总估计损失约 $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。背景GiddyVaultV30x5f0a…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 Contract2026年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 calltarget 必须固定到允许列表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 影响会留下一部分价值给剩余持有者。REVLoans0x2db6…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 并传入 cashOutFromdecimals 精度 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 询问会计上下文得到伪造的 (decimals36, currencyETH-code(61166))。由于源币种与目标币种相匹配REVLoans 走同币种快捷路径并跳过价格预言机将合法 terminal 的真实 ETH 余量按攻击者的 36 位小数目标单位重新表达使数值被放大 1e18 倍。图伪造 terminal 上的 accountingContextForTokenOf 返回 (decimals36, currency61166)**Step 3**REVLoans 把 (fake terminal, fake token) 注册进 _loanSourcesOf并把被夸大的数值写入 totalBorrowedFrom。伪造 terminal 仅以 “确认收到” 的方式 “放款”没有任何真实 ETH 移动。第一笔交易在 totalBorrowed 被向上操纵且仅烧掉少量 REV 抵押的状态下结束。图_addTo 中的运行时状态 —— REVLoanSource 推入 _loanSourcesOftotalBorrowedFrom 增加 36 位小数尺度的金额约 2.29e28**Step 4**攻击者再次调用 borrowFrom()这次把合法的 ETH terminal 作为 loan source仅以极小数量的 REV 作为抵押。cash-out 的数学计算这一次以真实的 18 位小数 ETH 单位运行。图Tx2 borrowFrom 的 Phalcon trace —— source.terminalJBMultiTerminalcurrentSurplusOf 在 decimals18 下运行**Step 5**在计算 totalBorrowed 时REVLoans 遍历 _loanSourcesOf 并触及 step 3 中的记录。由于该记录的 currency 仍与 ETH 匹配同币种快捷路径再次触发把以 36 位小数存储的余额折叠进 18 位小数的 ETH 求和中时放大了 1e18 倍。totalBorrowed 此时被假债务主导份额价格分子被极大膨胀。图运行时 _totalBorrowedFrom mulDiv 处理被污染的条目 —— x4.45e29 (36-decimal), y1e18, denominator1e18**Step 6**cash-out 的数学计算返回了一个由膨胀后的分子决定的借款金额该金额已被攻击者预先调整为略低于合法 terminal 的真实余量。合法 terminal 把这笔款付了出去几乎掏空了整个资金池。图运行时 cashOutFrom 返回约 23.15 ETH23,154,329,666,570,993,199 wei由膨胀的 totalBorrowed 推导而来结论根本原因是两个复合缺口(terminal, token) 未做 revnet 注册校验且同币种快捷路径在折叠余额时没有对 decimals 差异重新归一化。任何一个缺口单独存在危害都有限叠加后调用者可以注入任意 totalBorrowedFrom 记录并按面值兑出。修复对 (terminal, token) 做 revnet 注册校验并在折叠前按 source 存储的 decimals 精度重新归一化余额。Volo Vault2026年4月22日Sui 上的 yield vault 协议 Volo把用户存款路由到借贷协议 Navi 获取收益在 operator 私钥泄漏后损失约 $3.5M。金库合约本身没有代码层 bug攻击者只是以被盗凭据走了一遍合法的 operator 路径把 Volo 在 Navi 上的存款抽干。背景Volo 是面向用户的金库0xcd86…27fefaNavi 是底层借贷协议。金库持有 Navi AccountCapSui 的 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_v2Step 4攻击者用 bag::add 把 AccountCap 放回关闭操作并把资金转出。结论缺陷是结构性的一把 operator 密钥、完整提取权限、没有第二道检查。三种改动可以降低密钥泄漏的破坏。把 operator 角色拆到多签或门限方案泄漏的单一密钥无法独立授权提取。为对外提取加时间锁异常调用在结算前会留出可质疑的窗口。把 operator 的权限收窄到仅可存入与再平衡用户面提取走另一条独立路径泄漏的 operator 密钥就触及不到用户资金。Kipseli Router2026年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 Router0x579f…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 发起 tokenInWETH、tokenOutcbBTC 的调用。底层 AMM 不支持该路径但没有 revertquoter 0x592() 返回了 USDC 量级的 92,610,395约 92.61 USDC。图router 调用 quoter 询问 WETH→cbBTC 路径的 Phalcon trace —— staticcall 返回 USDC 量级的值Step 2router 把该值直接当作 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 量级的兜底值。Purrlend2026年4月25日HyperLiquid 与 MegaETH 上的借贷协议 Purrlend 因私钥泄漏损失约 $1.5M。攻击者接管 bridge 角色后铸出无底层资产支撑的 pTokensPurrlend 类 Aave 的存款凭证再以这些 pTokens 作为抵押借出资金池里的真实资产。背景Purrlend0x81d5…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 traceStep 2攻击者以 bridge 身份把大量 pTokens 铸到自己的地址。该铸造路径不做任何跨链托管核验新铸的 pTokens 没有任何底层资产支撑。Ste****p 3攻击者用这些无底层支撑的 pTokens 作为抵押。由于借款路径把任意 pToken 余额都当作合法供给头寸不会重新核验底层抵押检查通过WETH 被借出资金池。结论这是一次被合约层设计缺陷放大的私钥泄漏。被盗的密钥本身只赋予攻击者 bridge 角色的设计授权但这个授权包含了对 pToken 不受约束的铸造能力直接转化成可借出的抵押。两层都可以独立收紧。运营层把 bridge 角色拆到多签或门限方案单一密钥泄漏无法独立调用。合约层在铸造时要求传入可验证的跨链托管证明例如来自可信跨链验证器的消息承诺无证明即 revert。在铸造时验证证明是更彻底的修复因为它把对密钥托管的依赖整个去掉。SingularityFinance2026年4月26日Base 上的 SingularityFinance dynBaseUSDCv3 金库损失约 $413K。该金库被配置成使用一个不存在于 Uniswap V3 的 fee tier42导致每一个非 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 变量。该金库部署时配置 fee42不属于 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校验 fee42getPrice() 又在池不存在时返回 0 而非 revert。任意一层补上都足够在配置时校验预言机参数或在 getPool() 返回 address(0) 时 revert。作为纵深防御份额铸造逻辑可在接受存款前把 totalAssets() 与外部参考做一次健全性核对。Scallop2026年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 / stakesindex 随时间持续累加可以达到任意大的值。由于 update_points 又缺少绑定校验它能用这种异常 index 给任意账户写入一大笔 points 增量。这些被污染的 points 随后能在目标 spool 的 RewardsPool 上以 1:1 兑出因为账户本身合法绑定到目标 spoolredeem_rewards 的绑定校验通过。攻击分析以下分析基于交易 6WNDjC…NfVL。Step 1攻击者以 0.2 SUI 作为诱饵铸出 MarketCoin然后调用 new_spool_account stake 针对目标 spool 创建一个合法绑定的 SpoolAccountaccount.spool_id target_spool。Step 2攻击者调用 update_pointsMarketCoin(donor_spool, account, clock)donor_spool 传入一个被遗弃的 Spool。捐赠者的 index约 8.91e14被作为 points 写入账户points stake * (8.91e14 − 1.19e9) / 1e9 ≈ 1.62e14。Step 3攻击者调用 redeem_rewardsMarketCoin, 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。