Description
The function SettlementBranch::fillOffchainOrders is invoked by the Chainlink automation keeper to fulfill offchain orders. This function validates the price. As explained in the comment above this validation, the fill price should be less than or equal to the target price when buying, to avoid paying more than expected. Conversely, the fill price should be more than or equal to the target price when selling to prevent selling your tokens at a very low price. However, the condition checks the exact opposite and fills the offchain orders only when the limits are exceeded!
This leads users to buy only when the price is higher than their target, and sell when the price is lower than their target. This results in an unexpected loss of funds and a DoS of all legitimate orders (when the fill price is correct for users).
function fillOffchainOrders(
uint128 marketId,
OffchainOrder.Data[] calldata offchainOrders,
bytes calldata priceData
)
external
onlyOffchainOrdersKeeper(marketId)
{
...
for (uint256 i; i < offchainOrders.length; i++) {
...
@>
@>
@>
@> ctx.isFillPriceValid = (ctx.isBuyOrder && ctx.offchainOrder.targetPrice <= ctx.fillPriceX18.intoUint256())
@> || (!ctx.isBuyOrder && ctx.offchainOrder.targetPrice >= ctx.fillPriceX18.intoUint256());
if (!ctx.isFillPriceValid) {
continue;
}
...
}
}
Risk
Likelyhood: High
Impact: High
Orders are executed only if they exceed the target price, leading to financial loss for users.
Revert when the fill price is valid.
Recommended Mitigation
function fillOffchainOrders(
uint128 marketId,
OffchainOrder.Data[] calldata offchainOrders,
bytes calldata priceData
)
external
onlyOffchainOrdersKeeper(marketId)
{
...
for (uint256 i; i < offchainOrders.length; i++) {
...
// if the order increases the trading account's position (buy order), the fill price must be less than or
// equal to the target price, if it decreases the trading account's position (sell order), the fill price
// must be greater than or equal to the target price.
- ctx.isFillPriceValid = (ctx.isBuyOrder && ctx.offchainOrder.targetPrice <= ctx.fillPriceX18.intoUint256())
- || (!ctx.isBuyOrder && ctx.offchainOrder.targetPrice >= ctx.fillPriceX18.intoUint256());
+ ctx.isFillPriceValid = (ctx.isBuyOrder && ctx.offchainOrder.targetPrice >= ctx.fillPriceX18.intoUint256())
+ || (!ctx.isBuyOrder && ctx.offchainOrder.targetPrice <= ctx.fillPriceX18.intoUint256());
// we don't revert here because we want to continue filling other orders.
if (!ctx.isFillPriceValid) {
continue;
}
...
}
}