Assessment reports>Tradoor>High findings>Order-cancellation race condition may cause double payment
Category: Coding Mistakes

Order-cancellation race condition may cause double payment

High Impact
High Severity
Medium Likelihood

Description

The ExecuteOrCancelCompensate message handler allows the compensator entity to cancel an order and send funds to the order creator.

receive(msg: ExecuteOrCancelCompensate) {
    // [...]
    if(compensate.orderType != null) {
        if (compensate.orderType == ORDER_TYPE_LP) {
            self.liquidityOrders.del(compensate.orderId);
        } else {
            self.perpOrders.del(compensate.orderId);
            self.perpOrderExs.del(compensate.orderId);
        }
    }
    // [...]
}

However, it does not check to ensure an order exists. The Tact documentation specifies that the map's del function returns a boolean indicating whether the key was present in the map. So, if the key is not present, the user will be compensated even if the order does not exist.

A consequence of this is that — between the time when the compensator entity signs a transaction that calls the ExecuteOrCancelCompensate handler and it is actually executed on chain — there is the opportunity for an order to be canceled, executed, or otherwise deleted from the relevant map.

Note that a user can cancel their own order, meaning one aspect of the race condition may be controlled by the user. If the user has knowledge that the compensator will cancel their order, they may try to race the compensator to cancel the order themselves and thereby receive the funds twice.

Impact

In the worst-case scenario, a malicious attacker may be able to receive a double payment for canceling their order, thereby stealing funds from the protocol. This may also happen by accident if the protocol is very active.

Recommendations

Assert that the calls to del return true:

receive(msg: ExecuteOrCancelCompensate) {
    // [...]
    if(compensate.orderType != null) {
        if (compensate.orderType == ORDER_TYPE_LP) {
-            self.liquidityOrders.del(compensate.orderId);
+            require(self.liquidityOrders.del(compensate.orderId), "order not found");
        } else {
-            self.perpOrders.del(compensate.orderId);
-            self.perpOrderExs.del(compensate.orderId);
+            require(self.perpOrders.del(compensate.orderId), "order not found");
+            require(self.perpOrderExs.del(compensate.orderId), "order not found");
        }
    }
    // [...]
}

Remediation

This finding was addressed in the TON pool contract in commit 68b5af, and in the USDT pool in commit 0d6693 by implementing the suggested recommendation.

Zellic © 2025Back to top ↑