Skip to content

Latest commit

 

History

History
115 lines (74 loc) · 4.67 KB

File metadata and controls

115 lines (74 loc) · 4.67 KB

Mammoth Mocha Koala

Medium

Double Subtraction of Fees

Summary

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/libs/CheckpointLib.sol#L57C1-L98C6

The Double Subtraction of Fees issue occurs when fees already accounted for in the previous checkpoint’s collateral balance are subtracted again when computing the next checkpoint’s collateral. This results in fees being deducted twice, artificially reducing the collateral balance.

Root Cause

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/libs/CheckpointLib.sol#L57C1-L98C6

library CheckpointLib { /// @notice Accumulate pnl and fees from the latest position to next position /// @param order The next order /// @param fromVersion The previous latest version /// @param toVersion The next latest version /// @return next The next checkpoint /// @return response The accumulated pnl and fees function accumulate( IMarket.Context memory context, IMarket.SettlementContext memory settlementContext, uint256 orderId, Order memory order, Guarantee memory guarantee, Version memory fromVersion, Version memory toVersion ) external returns (Checkpoint memory next, CheckpointAccumulationResponse memory) { CheckpointAccumulationResult memory result;

    // accumulate
    result.collateral = _accumulateCollateral(context.latestPositionLocal, fromVersion, toVersion);
    result.priceOverride = _accumulatePriceOverride(guarantee, toVersion);
    (result.tradeFee, result.subtractiveFee, result.solverFee) = _accumulateFee(order, guarantee, toVersion);
    result.offset = _accumulateOffset(order, guarantee, toVersion);
    result.settlementFee = _accumulateSettlementFee(order, guarantee, toVersion);
    result.liquidationFee = _accumulateLiquidationFee(order, toVersion);


    // update checkpoint
    next.collateral = settlementContext.latestCheckpoint.collateral
        .sub(settlementContext.latestCheckpoint.tradeFee)                       // trade fee processed post settlement
        .sub(Fixed6Lib.from(settlementContext.latestCheckpoint.settlementFee)); // settlement / liquidation fee processed post settlement
    next.collateral = next.collateral
        .add(settlementContext.latestCheckpoint.transfer)                       // deposit / withdrawal processed post settlement
        .add(result.collateral)                                                 // incorporate collateral change at this settlement
        .add(result.priceOverride);                                             // incorporate price override pnl at this settlement
    next.transfer = order.collateral;
    next.tradeFee = Fixed6Lib.from(result.tradeFee).add(result.offset);
    next.settlementFee = result.settlementFee.add(result.liquidationFee);


    emit IMarket.AccountPositionProcessed(context.account, orderId, order, result);


    return (next, _response(result));
}

Each checkpoint’s collateral value is intended to reflect the net balance after all prior fees (e.g., tradeFee, settlementFee) have been applied.

For example, if the previous checkpoint’s collateral is 100 and a tradeFee of 10 was applied, the collateral in that checkpoint should already be 90 (i.e., 100 - 10).

next.collateral = settlementContext.latestCheckpoint.collateral .sub(settlementContext.latestCheckpoint.tradeFee) // ❌ Subtracts fees again .sub(Fixed6Lib.from(settlementContext.latestCheckpoint.settlementFee)); // ❌ Here, the code subtracts the previous checkpoint’s tradeFee and settlementFee from latestCheckpoint.collateral, which already includes these deductions from prior computations. This double-counts the fees.

Example Initial State:

latestCheckpoint.collateral = 100

latestCheckpoint.tradeFee = 10 (already deducted to reach 100).

Erroneous Calculation:

next.collateral = 100 (previous collateral) - 10 (tradeFee) = 90.

Result: Collateral becomes 90, but it should remain 100 (since the 10 fee was already subtracted in the prior checkpoint).

Impact:

Collateral is reduced by 10 again, leading to an incorrect balance of 90 instead of 100.

Over time, this error compounds, disproportionately penalizing users by over-deducting fees.

Internal Pre-conditions

No response

External Pre-conditions

No response

Attack Path

No response

Impact

Over time, this error compounds, disproportionately penalizing users by over-deducting fees.

PoC

No response

Mitigation

Remove the redundant subtraction of prior fees when computing next.collateral: