1100万美元的Cork协议攻击:Uniswap V4 Hook 安全性的重要教训

  • Dedaub
  • 发布于 2025-05-31 18:41
  • 阅读 41

Cork Protocol 因 Uniswap V4 hook 实现中的访问控制漏洞遭受了 1100 万美元的攻击。

Dedaub

1100 万美元的 Cork 协议攻击:Uniswap V4 Hook 安全性的重要教训

在 2025 年 5 月 28 日,Cork 协议遭受了一次价值 1100 万美元的攻击,原因是多个安全漏洞,最终导致其 Uniswap V4 Hook 实现中的一个关键访问控制漏洞。攻击者利用 Hook 的回调函数中缺失的验证,欺骗协议,使其认为有价值的 代币 (Redemption Assets) 是由攻击者存入的,从而向攻击者授予了大量可以兑换回其他有价值 代币 的衍生 代币。攻击者还利用了风险溢价计算,该计算加剧了攻击。除其他事项外,此事件强调了 Uniswap V4 Hook 中适当访问控制的重要性,以及高度灵活的开放设计的风险,这些设计很难保障安全。

背景

了解 Cork 协议

Cork 协议是一个建立在 Uniswap V4 上的 脱锚 保险平台,允许用户对 稳定币 或流动性质押 代币脱锚 进行 对冲。该协议在每个市场运营四种 代币 类型:

  • RA (Redemption Asset): “原始”资产(例如,wstETH)
  • PA (Pegged Asset): “风险较高”的 锚定 资产(例如,weETH)
  • DS (Depeg Swap): 如果 PA 从 RA 脱锚,则支付的保险 代币
  • CT (Cover Token): 赚取收益但如果发生 脱锚 会损失价值的 对冲 头寸

另一种考虑 DS 的方式是:以 RA 计价的固定执行价格的看跌期权,而 CT 是相应的空头看跌期权。

用户可以通过存入 RA 来 铸造 DS + CT,有效地将赎回资产分成两个互补的头寸。可以在这里找到一个展示这一点的合法交易。

与 Opyn 等现代期权协议不同,DS 由 RA 完全 抵押,这简化了信任假设。

了解 Uniswap V4

Uniswap V4 代表着一个重大的架构转变,转向一个中央 PoolManager (Singleton 设计模式),并引入了 "Hook"——外部合约,PoolManager 的生命周期的各个时间点调用这些合约(例如,在 交换、流动性变化之前或之后)。正如 Damien Rusinek 等安全专家强调 的那样,这种设计提供了极大的灵活性和自定义性,但正如 Cork 协议事件所表明的那样,也为开发者引入了新的、关键的安全注意事项。

漏洞 1:缺少访问控制

CorkHook 合约中的一个重要漏洞是一个关键的疏忽,直接呼应了许多安全研究人员警告过的常见陷阱。攻击者的智能合约在交易中直接调用了 Cork 的 Uniswap Hook。让我们检查一下存在漏洞的 beforeSwap 函数:

function beforeSwap(
    address sender,
    PoolKey calldata key,
    IPoolManager.SwapParams calldata params,
    bytes calldata hookData
) external override returns (bytes4, BeforeSwapDelta delta, uint24) {
    PoolState storage self = pool[toAmmId(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1))];
    // kinda packed, avoid stack too deep
    delta = toBeforeSwapDelta(-int128(params.amountSpecified), int128(_beforeSwap(self, params, hookData, sender)));
    // TODO: do we really need to specify the fee here?
    return (this.beforeSwap.selector, delta, 0);
}

关键问题:此函数缺少 onlyPoolManager 修饰符(仅允许从受信任的 Uniswap v4 管理器 进行调用),这意味着任何人都可以使用任意参数直接调用它。虽然该合约继承自 BaseHook,它为 unlockCallback 提供了访问控制,但它未能保护其他 Hook 回调。

// BaseHook provides this for unlockCallback:
// BaseHook 为 unlockCallback 提供了这个:
modifier onlyPoolManager() {
    require(msg.sender == address(poolManager), "Caller not pool manager"); _;
}

漏洞 2:风险溢价计算翻转

风险溢价会影响衍生品 (CT) 代币 的价格,在接近到期时具有极端值。 攻击者在接近到期时获得了少量的 DS 代币,从而操纵了 CT 与 RA 代币 的价格比率。在 翻转 (对于新的到期期间)时,这种倾斜的比率用于计算要向 AMM 存入多少 CT 和 RA 代币。由于存入的 CT 与 RA 代币 的比率倾斜,攻击者 可以将极少量的 0.0000029 wstETH 转换为 3760.8813 weETH-CT。

攻击

Cork 协议允许一个市场的 DS(保险)代币 用作另一个市场的 RA(安全资产)代币。这可能不是一个有意的设计选择,并且协议作者可能没有考虑到这种可能性。一个无意的后果是,如果存在漏洞,则可以从另一个市场访问来自良好市场的相对有价值的 代币(DS 代币)。

这种相对模糊的安全漏洞加剧了此次攻击者发起的非常复杂的、多步骤的攻击。

第 1 步:跨市场 代币 混淆

攻击者创建了一个新的市场配置,该配置使用另一个市场中的一个 DS 代币 作为新市场中的 RA 代币

// Legitimate market
// 合法市场
Legit Market: {
    RA: wstETH,
    PA: weETH,
    DS: weETH-DS,
    CT: weETH-CT
}

// Attacker's new market
// 攻击者的新市场
New Market: {
    RA: weETH-DS, // Using DS token as RA!
    // 使用 DS 代币作为 RA!
    PA: wstETH,
    DS: new_ds,
    CT: new_ct
}

第 2 步:恶意 Hook 合约

攻击者部署了自己的合约,该合约实现了 Hook 接口和 费率提供商接口。 自定义费率提供商在此攻击中似乎是 转移注意力 的东西——它只是返回一个固定费率。

新市场使用了一个新的 Uniswap v4 ,它是作为新市场的一部分创建的。攻击者还(在单独的交易中)创建了一个 Uniswap ,该 具有与新创建的 相同的 代币(交易 new_CT 和 weETH-DS),但攻击者的合约作为 Hook

第 3 步:直接 Hook 操作

这就是行动发生的地方。由于缺少访问控制,攻击者可以直接调用 beforeSwap 来欺骗协议:

这个恶意创建的 ID 被传递到 beforeSwap 回调中。作为回调一部分提供的 Hook 数据指示协议执行 RA 被存入并返回 CT 和 DS 代币 的执行流程。但是,在这样的交易中,攻击者没有存入任何 RA。相反,大约 3761 个 weETH-DS 被记入攻击者的 借方。精心设计的 Hook 数据 有效载荷 欺骗 Cork 协议,使其认为攻击者已存入 3761 个 weETH-DS。通过这样做,攻击者非法获得 3761 个 new_ct 和 3761 个 new_ds 代币

第 4 步:DS 代币 提取

一旦攻击者获得了 new_ct 和 new_ds 代币,攻击者就使用这些 代币 兑换 weETH-DS 代币

第 5 步:wstETH 代币 提取

请注意,在之前的步骤中,攻击者还利用了另一个 边缘案例 以廉价地获得 weETH-CT 代币。自从撰写本文以来,Cork 协议团队发布了一个更清晰的解释,说明了所涉及的错误计算,但本质是,攻击者 在接近到期时获得了少量的 DS 代币,从而操纵了下一个到期期间 CT 与 RA 代币 的价格比率。通过这种操纵,攻击者 可以将 0.0000029 wstETH(一个非常小的数量)转换为 3760.8813 weETH-CT。

现在,攻击者要做的就是按计划通过协议赎回这些 weETH-CT 和 weETH-DS 代币,以提取价值 1100 万美元的 wstETH。

技术深入探讨:Hook 操作

_beforeSwap 函数包含用于处理 交换 的复杂逻辑,包括储备更新和 费用 计算:

function _beforeSwap(
  PoolState storage self,
  IPoolManager.SwapParams calldata params,
  bytes calldata hookData,
  address sender
) internal returns (int256 unspecificiedAmount) {
    // ... swap calculations ...
    // ... 交换计算 ...
    // Update reserves without validation
    // 在没有验证的情况下更新储备
    self.updateReservesAsNative(Currency.unwrap(output), amountOut, true);
    // Settle tokens
    // 结算代币
    settleNormalized(output, poolManager, address(this), amountOut, true);
    // ... more logic ...
    // ... 更多逻辑 ...
}

在没有访问控制的情况下,攻击者可以:

  • 在合法交易之前操纵 储备 比率
  • 强制 Hook 用任意金额结算 代币
  • 绕过通过 PoolManager 的正常 交换 路由

解析hookData 中使用的参数,攻击者制作了一个旨在表明他们已将 3761 个 weETH-DS 代币 存入新市场的 有效载荷

促成因素

1. 去中心化市场创建

该协议允许任何人使用任何 代币对 创建市场。 这是一个大胆的设计决策,但很明显,很难正确完成。

function beforeInitialize(address, PoolKey calldata key, uint160) external ... {
    address token0 = Currency.unwrap(key.currency0);
    address token1 = Currency.unwrap(key.currency1);

    // Dedaub: No validation on token types!
    // Dedaub:没有对代币类型进行验证!
    // Allows DS tokens to be used as RA tokens
    // 允许 DS 代币用作 RA 代币

}

2. 代币 验证不足

_saveIssuedAndMaturationTime 函数尝试验证 代币,但未能确保正确的 代币 类型:

function _saveIssuedAndMaturationTime(PoolState storage self) internal {
    IExpiry token0 = IExpiry(self.token0);
    IExpiry token1 = IExpiry(self.token1);
    // Dedaub: Only checks if tokens have expiry, not their type
    // Dedaub:仅检查 代币 是否有到期日,而不是它们的类型
    try token0.issuedAt() returns (uint256 issuedAt0) {
        self.startTimestamp = issuedAt0;
        self.endTimestamp = token0.expiry();
        return;
    } catch {}
    // ... similar for token1 ...
    // ... token1 类似 ...
}

3. 没有 白名单

回调允许具有相同 代币,但 Hook 合约不同的 。没有对 ID 和 Hook 合约地址进行验证。

mapping(PoolId => bool) public allowedPools;

modifier onlyAllowedPool(PoolKey calldata key) {
    require(allowedPools[key.toId()], "Pool not allowed");
    _;
}

4. 单例 设计

来自不同市场的 代币 混合在一起(单例 模式)。因此,应用于新市场的漏洞设法提取了与另一个市场相关的 代币

之前的 Cork 协议审计

不幸的是,尽管 Cork 协议已经接受了四家不同审计提供商的安全审查,但此事件仍然发生了。该协议团队显然在安全方面投入了资源,使得这次攻击对于团队和用户来说更加悲惨。

然而,在四家审计师中,三家没有审计存在漏洞的 Hook 合约,并且不确定仅通过查看代码是否可以轻松发现风险溢价问题。Cantina/Spearbit 很可能在其审计范围内的易受攻击的 CorkHook 合约。包含建议的 pull request 表明他们确实发现了一些问题并提出了改进建议。

Runtime Verification(另一家没有将 CorkHook 纳入其范围的审计师)在其报告中先见之明地指出:

“一个有趣的后续参与将是证明由本次参与范围内验证的不同组件调用的 CorkHook 函数的不变量,以及其他合约(例如 CorkHook、Liquidator 和 HedgeUnit)的函数的不变量。”

现在看来,这一观察结果特别具有预见性,因为正是 CorkHook 与其他组件的交互促成了这次攻击。

Hook 开发者的建议

如果你正在构建一个以有意义的方式与 Uniswap v4 Hook 交互的项目,请让该领域的专家审计你的代码。Dedaub 是 Uniswap 白名单审计提供商,拥有大量保护高风险项目经验。由于 Dedaub 已被 Uniswap 列入白名单,因此也可以通过 Uniswap 基金会赠款支付审计费用。同时,请遵循以下准则。我们还建议听取 Damien Rusinek 的谈话。

掌握访问控制和权限

严格的 PoolManager-Only 访问权限: 这是不容商量的。每个可以修改状态或旨在由 PoolManager 调用的外部 Hook 函数(例如,beforeSwapafterSwapbeforeInitialize必须 实现强大的访问控制,通常是 onlyPoolManager 修饰符。这是 Cork 攻击中的一个主要失败。正如 Damien 和 Hacken 所强调的那样,允许任意地址直接调用是操纵状态和资金损失的直接途径。Cork 没有遵循这个建议。

正确的 Hook 地址配置: Uniswap V4 直接从 Hook 合约的地址派生 Hook 权限(PoolManager 将调用的函数)。

地址挖掘: 使用 CREATE2 部署 Hook,其中 salt 确保部署的地址正确编码所有 预期的权限(例如,Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG)。Cork 没有遵循这个建议。

避免不匹配: 你的 Hook 中实现的功能与其地址中编码的权限之间的不匹配将导致函数未被调用或 PoolManager 尝试调用不存在的函数,从而导致还原 (DoS)。

面向未来的升级: 如果你计划在将来的升级中添加新的可 Hook 函数(对于 UUPS 样式的 代理),请确保初始部署地址已经编码了这些将来的权限。或者,为它们包含 占位符 函数。

继承自 BaseHook 尽可能从 Uniswap 的 BaseHook 合约继承。它提供了基础安全检查(例如 unlockCallbackonlyPoolManager),并有助于确保正确的接口遵守,从而降低了配置错误的风险。

严格的状态管理和 交互

限制 如果 Hook 专为特定 或一组 设计,则必须 在其函数中(尤其是在初始化中)验证 PoolKey,以防止未经授权的 使用它。考虑实现 allowedPools 映射 和类似 onlyAllowedPool修饰符。确保 Hook 只能初始化一次(例如,在 beforeInitialize 中),以将其限制为单个 (如果这是设计)。Cork 没有遵循这个建议。

隔离可重用 Hook 的状态: 如果打算在多个合法 之间共享 Hook,则必须仔细隔离其内部状态(例如,使用 mapping(PoolId => PoolSpecificData))。否则可能导致一个 的活动破坏另一个 的状态,从而可能锁定资金或造成可利用的条件。

防止跨市场 代币 污染: 正如在 Cork 攻击中看到的那样,避免设计中一个市场的 代币(尤其是衍生品或 抵押品 等敏感 代币)被误解或误用为另一个市场中的不同 代币 类型。在市场创建和 Hook 逻辑中强制执行严格的 代币 类型验证。

了解 sendermsg.sender 与交易发起者。Hook 函数(如 beforeSwap(address sender, ...))中,sender 参数通常是 PoolOperatorPoolManager 本身,而不是 发起交易的最终用户 (EOA)。如果你的 Hook 逻辑需要实际的最终用户,则必须由受信任的 PoolOperator 通过 hookData 参数安全地传递该地址。

了解 Delta 会计 BeforeSwapDeltaBalanceDelta 来自 Hook 的角度。如果 Hook 收取 费用,则必须为 delta。如果它提供 回扣,则为 delta。根据 交换 方向 (params.zeroForOne) 确保 代币 delta 的正确顺序(例如,指定与未指定,或 token0token1)。至关重要的是,所有 delta 必须在 unlockCallback 结束时 总计为零 PoolManager 使用 NonzeroDeltaCount 跟踪此信息。未结余额将导致交易恢复。修改余额的 Hook 必须确保它们(或用户)正确结算这些金额(例如,通过 settle()take())。

可升级性: 如果你的 Hook 是可升级的,请将其视为一个重要的信任假设。恶意或受损的所有者可以完全更改 Hook 的逻辑。确保升级机制是安全的并且透明地管理。

结论

Cork 协议攻击表明,Uniswap V4 Hook 虽然功能强大,但引入了新的安全注意事项,开发人员必须认真对待。缺少访问控制和 代币 验证不足的结合为利用创造了完美的风暴。随着 DeFi 生态系统 继续发展,出现更多可组合的协议,开发人员必须优先考虑其架构每一层的安全性。

  • 原文链接: dedaub.com/blog/the-11m-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dedaub
Dedaub
Security audits, static analysis, realtime threat monitoring