DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: medium
Valid

Filling of OffchainOrders can be DOSsed

Summary

A single user or group of users with offchain orders can cause unnecessary DOS of other unsuspecting users with offChainOrders to the same marketId

Vulnerability Details

Offchain orders provide a means for users to create orders offchain, sign them then broadcast these to the zaros network which uses keepers to execute these orders.
The keepers take all signed offchain orders to a single marketId and fill them within a loop.

function fillOffchainOrders(
uint128 marketId,
OffchainOrder.Data[] calldata offchainOrders,
bytes calldata priceData
)
external
onlyOffchainOrdersKeeper(marketId)
{
// working data
FillOffchainOrders_Context memory ctx;
// fetch storage slot for perp market's offchain order config
SettlementConfiguration.Data storage settlementConfiguration =
SettlementConfiguration.load(marketId, SettlementConfiguration.OFFCHAIN_ORDERS_CONFIGURATION_ID);
// fetch storage slot for perp market
PerpMarket.Data storage perpMarket = PerpMarket.load(marketId);
// fetch storage slot for global config
GlobalConfiguration.Data storage globalConfiguration = GlobalConfiguration.load();
(ctx.bidX18, ctx.askX18) =
settlementConfiguration.verifyOffchainPrice(priceData, globalConfiguration.maxVerificationDelay);
for (uint256 i; i < offchainOrders.length; i++) {
ctx.offchainOrder = offchainOrders[i];
// enforce size > 0
if (ctx.offchainOrder.sizeDelta == 0) {
revert Errors.ZeroInput("offchainOrder.sizeDelta");
}
// load existing trading account; reverts for non-existent account
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExisting(ctx.offchainOrder.tradingAccountId);
// enforce that keeper is filling the order for the correct marketId
if (marketId != ctx.offchainOrder.marketId) {
revert Errors.OrderMarketIdMismatch(marketId, ctx.offchainOrder.marketId);
}
@> if (ctx.offchainOrder.nonce != tradingAccount.nonce) {
revert Errors.InvalidSignedNonce(tradingAccount.nonce, ctx.offchainOrder.nonce);
}
..........................................................
}

However, due to the nature of executing these orders within a loop, an error in one of the orders would cause a failure in filling all orders. These may be unintentional or malicious. One of these errors which can be manipulated by a user to DOS offchain orders filling is the check for offchainOrder nonce.
Users can cancel their offchain orders at any time by calling OrderBranch.cancelAllOffchainOrders. This increments the tradingAccount nonce such that all offchain orders for that tradingAccount become invalid

function cancelAllOffchainOrders(uint128 tradingAccountId) external {
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExistingAccountAndVerifySender(tradingAccountId);
unchecked {
tradingAccount.nonce++;
}
emit LogCancelAllOffchainOrders(msg.sender, tradingAccountId, tradingAccount.nonce);
}

The user may DOS the fillOrder operation by waiting for the keeper to pick up their order and call fillOffchainOrders. By frontrunning this call, they cause this action of the keeper to fail. Note that there're other simple methods the user or group of users can achieve this DOS. Also, this can be kept up for as long as necessary by using numerous trading accounts with offchainorders and targetted to as many markets as the attacker intends.

Impact

Offchain orders can be DOSsed for as long as an attacker intends. All active markets can be affected and this is a cheap and simple exploit to carry out. Due to failure of filling offchain orders, normal operations of the zaros protocol would fail and users may even fall into liquidation or lose funds.

Tools Used

Manual Review

Recommendations

Instead of reverting, consider using continue such that the order is skipped. This can be done for other points of failure too

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

fillOffchainOrders reverts everything if a single order fails one of the multiple checks

If you send 1 cancel and 1 create it should still run the cancel, not revert everything.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.