文章指出,一个去中心化交易所(DEX)的_cancelOrder()函数在取消订单后,未能完全清除订单在链表中的prev、next和remaining字段。这导致链表腐败,允许攻击者对同一订单进行多次退款,从而可能造成DEX资金池枯竭,甚至结合其他漏洞窃取用户资金。文章提出了清晰的修复建议。
由安全研究员提交
内部函数 _cancelOrder() 在从链表中解除链接后,未能清除订单的 prev、next 和 remaining 字段。这使得同一订单可以被多次处理,从而导致双重退款攻击,造成 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);
}
陈旧的 prev 和 next 指针会创建一个可被利用的损坏链表状态。
对 StablecoinDEX 偿付能力的严重影响:
双重退款攻击:通过利用陈旧指针,攻击者可以两次触发同一订单的取消逻辑,获得双重退款。
DEX 资不抵债:合约记录的余额高于实际代币持有量,导致后续用户无法提款。
与 H-02 结合:利用未经授权的取消漏洞,攻击者可以双重取消受害者的订单,窃取其锁定的资金。
记账腐败:totalDeposits 跟踪变得不准确,破坏了 DEX 分析和价格发现。
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
}
清除订单指针解除链接后,以防止陈旧引用漏洞:
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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!