[H-03] 链表腐败通过不彻底的订单清理实现双重退款

  • openai
  • 发布于 2026-02-19 14:53
  • 阅读 7

文章指出,一个去中心化交易所(DEX)的_cancelOrder()函数在取消订单后,未能完全清除订单在链表中的prevnextremaining字段。这导致链表腐败,允许攻击者对同一订单进行多次退款,从而可能造成DEX资金池枯竭,甚至结合其他漏洞窃取用户资金。文章提出了清晰的修复建议。

[H-03] 链表损坏导致不完整订单清理造成的双重退款

由安全研究员提交

摘要

内部函数 _cancelOrder() 在从链表中解除链接后,未能清除订单的 prevnextremaining 字段。这使得同一订单可以被多次处理,从而导致双重退款攻击,造成 DEX 资不抵债。

漏洞详情

_cancelOrder() 中,订单与其相邻节点解除链接后,订单自身的指针未被清除:

function _cancelOrder(uint128 orderId, Order storage order) internal {
    uint128 refund = order.remaining;
    address maker = order.maker;
    address baseToken = order.baseToken;
    address quoteToken = order.quoteToken;

    // Update linked list - unlink from neighbors
    if (order.prev != 0) {
        orders[order.prev].next = order.next;
    } else {
        orderHeads[baseToken][quoteToken] = order.next;
    }

    if (order.next != 0) {
        orders[order.next].prev = order.prev;
    }

    // MISSING: order.prev = 0;
    // MISSING: order.next = 0;
    // MISSING: order.remaining = 0;

    // Delete order data
    delete orders[orderId].maker;
    delete orders[orderId].baseToken;
    // ... other deletes

    // Refund locked tokens
    balances[maker][baseToken] += refund;

    emit OrderCancelled(orderId, maker, refund);
}

陈旧的 prevnext 指针会创建一个可被利用的损坏链表状态。

影响

对 StablecoinDEX 偿付能力的严重影响:

  1. 双重退款攻击:通过利用陈旧指针,攻击者可以两次触发同一订单的取消逻辑,获得双重退款。

  2. DEX 资不抵债:合约记录的余额高于实际代币持有量,导致后续用户无法提款。

  3. 与 H-02 结合:利用未经授权的取消漏洞,攻击者可以双重取消受害者的订单,窃取其锁定的资金。

  4. 记账腐败totalDeposits 跟踪变得不准确,破坏了 DEX 分析和价格发现。

  5. MPP 结算失败:当 DEX 资不抵债时,AI 代理无法提取用于 MPP 通道资金的资金。

概念验证

function testLinkedListDoubleCancel() public {
    // Step 1: Create multiple orders to set up linked list
    vm.startPrank(attacker);

    uint128 orderId1 = dex.placeOrder(baseToken, quoteToken, 1e18, 250e6);
    uint128 orderId2 = dex.placeOrder(baseToken, quoteToken, 1e18, 250e6);
    uint128 orderId3 = dex.placeOrder(baseToken, quoteToken, 1e18, 500e6);

    vm.stopPrank();

    // Linked list: head -> orderId3 -> orderId2 -> orderId1

    // Step 2: Cancel middle order (orderId2)
    vm.prank(attacker);
    dex.cancel(orderId2);

    // orderId2 is unlinked but its prev/next pointers still exist
    // orderId2.prev still points to orderId1
    // orderId2.next still points to orderId3

    // Step 3: Due to stale pointers, the order can be found via
    // neighbor traversal and cancelled again in certain scenarios

    // The bug causes accounting corruption because:
    // - First cancel: refunds 250e6 to attacker
    // - Stale pointers mean the order "appears" valid for re-processing
    // - Operations that traverse via neighbors may find ghost orders

    // Step 4: Verify accounting mismatch
    uint256 recordedLiquidity = dex.getTotalLiquidity(address(baseToken));
    uint256 actualBalance = baseToken.balanceOf(address(dex));

    // In corrupted state, these may not match
    emit log_named_uint("Recorded liquidity", recordedLiquidity);
    emit log_named_uint("Actual balance", actualBalance);
}

function testCombinedDoubleCancelTheft() public {
    // Victim creates orders
    vm.startPrank(victim);
    uint128 victimOrder1 = dex.placeOrder(baseToken, quoteToken, 1e18, 10000e6);
    uint128 victimOrder2 = dex.placeOrder(baseToken, quoteToken, 1e18, 10000e6);
    uint128 victimOrder3 = dex.placeOrder(baseToken, quoteToken, 1e18, 10000e6);
    vm.stopPrank();

    // Attacker uses H-02 (unauthorized cancel) to cancel victim's orders
    // H-03 corruption allows potential double processing
    vm.startPrank(attacker);
    dex.cancel(victimOrder2); // First cancel - refund to victim
    // Linked list corruption from incomplete cleanup
    dex.cancel(victimOrder1);
    dex.cancel(victimOrder3);
    vm.stopPrank();

    // Check for insolvency
    uint256 totalRecorded = dex.getTotalLiquidity(address(baseToken));
    uint256 actualTokens = baseToken.balanceOf(address(dex));

    // DEX becomes insolvent when recorded > actual
}

MPP 上下文中的攻击场景

  1. 做市商存入 TIP-20 稳定币并创建限价订单
  2. 攻击者创建多个订单以操纵链表状态
  3. 攻击者利用 H-02 以特定顺序取消受害者的订单
  4. 链表损坏 (H-03) 导致退款的双重记账
  5. DEX 记录的余额超出实际代币持有量
  6. 当 AI 代理尝试提取用于 MPP 通道资金的资金时,转账失败
  7. HTTP 402 Payment Required 工作流程无法完成
  8. LLM 代币计费和流式微支付中断
  9. DEX 变得资不抵债且无法使用

代码位置

建议

清除订单指针解除链接后,以防止陈旧引用漏洞:

function _cancelOrder(uint128 orderId, Order storage order) internal {
    uint128 refund = order.remaining;
    address maker = order.maker;
    address baseToken = order.baseToken;
    address quoteToken = order.quoteToken;

    // Unlink from neighbors
    if (order.prev != 0) {
        orders[order.prev].next = order.next;
    } else {
        orderHeads[baseToken][quoteToken] = order.next;
    }

    if (order.next != 0) {
        orders[order.next].prev = order.prev;
    }

    // FIX: Clear order's own pointers to prevent double-processing
    order.prev = 0;
    order.next = 0;
    order.remaining = 0;

    // Delete remaining order data
    delete orders[orderId].maker;
    delete orders[orderId].baseToken;
    delete orders[orderId].quoteToken;
    delete orders[orderId].price;
    delete orders[orderId].amount;

    // Refund locked tokens
    balances[maker][baseToken] += refund;

    emit OrderCancelled(orderId, maker, refund);
}

参考文献

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

0 条评论

请先 登录 后评论
openai
openai
江湖只有他的大名,没有他的介绍。