Skip to content

Latest commit

 

History

History
143 lines (86 loc) · 5.2 KB

File metadata and controls

143 lines (86 loc) · 5.2 KB

Mammoth Mocha Koala

Medium

Invalid Oracle Handling and Position Updates

Summary

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/Market.sol#L716C5-L774C6

When an order is added to pending, both global and local positions are immediately updated with the order's amounts.

During settlement, if the oracle version is invalid, the order is invalidated (amounts set to zero), and the pending is subtracted.

However, the positions (global and local) are not reverted to their pre-order state. This results in positions being permanently increased by invalid orders, breaking the invariant that positions should only reflect valid, settled orders

Root Cause

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/Market.sol#L716C5-L774C6

function _update(
    Context memory context,
    UpdateContext memory updateContext,
    Order memory newOrder,
    Guarantee memory newGuarantee,
    address orderReferrer,
    address guaranteeReferrer
) private notSettleOnly(context) {
    // advance to next id if applicable
    if (context.currentTimestamp > updateContext.orderLocal.timestamp) {
        updateContext.orderLocal.next(context.currentTimestamp);
        updateContext.guaranteeLocal.next();
        updateContext.liquidator = address(0);
        updateContext.orderReferrer = address(0);
        updateContext.guaranteeReferrer = address(0);
        context.local.currentId++;
    }
    if (context.currentTimestamp > updateContext.orderGlobal.timestamp) {
        updateContext.orderGlobal.next(context.currentTimestamp);
        updateContext.guaranteeGlobal.next();
        context.global.currentId++;
    }


    // update current position
    updateContext.currentPositionGlobal.update(newOrder);
    updateContext.currentPositionLocal.update(newOrder);


    // apply new order
    updateContext.orderLocal.add(newOrder);
    updateContext.orderGlobal.add(newOrder);
    context.pendingGlobal.add(newOrder);
    context.pendingLocal.add(newOrder);
    updateContext.guaranteeGlobal.add(newGuarantee);
    updateContext.guaranteeLocal.add(newGuarantee);


    // update collateral
    context.local.update(newOrder.collateral);


    // protect account
    if (newOrder.protected()) updateContext.liquidator = msg.sender;


    // apply referrer
    _processReferrer(updateContext, newOrder, newGuarantee, orderReferrer, guaranteeReferrer);


    // request version, only request new price on non-empty market order
    if (!newOrder.isEmpty() && newGuarantee.isEmpty()) oracle.request(IMarket(this), context.account);


    // after
    InvariantLib.validate(context, updateContext, newOrder, newGuarantee);


    // store
    _storeUpdateContext(context, updateContext);


    // fund
    if (newOrder.collateral.sign() == 1) token.pull(msg.sender, UFixed18Lib.from(newOrder.collateral.abs()));
    if (newOrder.collateral.sign() == -1) token.push(msg.sender, UFixed18Lib.from(newOrder.collateral.abs()));


    // events
}

When a new order is submitted (via _update), both the global (context.latestPositionGlobal) and local (context.latestPositionLocal) positions are immediately updated with the order’s amounts (e.g., maker, long, short).

Example: A user opens a long position of +100. The global/long position increases by 100 instantly.

The order is added to pendingGlobal/pendingLocal to await settlement.

During settlement (_processOrderGlobal/_processOrderLocal), the code checks if the oracle version associated with the order is valid:

if (!oracleVersion.valid) newOrder.invalidate(newGuarantee); If invalid, the order is "invalidated" (its amounts are zeroed out).

The invalidated order (now zeroed) is subtracted from the pending aggregate:

context.pendingGlobal.sub(newOrder); // Subtracts zero Position Not Reverted:

The global/local positions (context.latestPositionGlobal/context.latestPositionLocal) remain updated with the original (now invalid) order amounts.

The positions are never rolled back to their pre-order state.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

A user submits an order increasing their position by X.

The pending and current positions are updated to include X.

Settlement finds the oracle version invalid, invalidates the order (sets amounts to zero), subtracts the pending, but doesn't revert the position.

The user's position remains increased by X, despite the order being invalid, leading to incorrect accounting.

Impact

This allows invalid orders to permanently alter positions, leading to incorrect collateral requirements, exposure calculations, and potential financial exploitation.

PoC

No response

Mitigation

When invalidating an order during settlement, revert the position changes made when the order was initially added. This can be done by subtracting the original order amounts from the current positions during invalidation.

Only update positions after an order is validated during settlement, not when it’s initially placed.