HIGH severity: Intent orders are guaranteed to execute, but fees from these orders are not accounted in collateral, allowing user to withdraw all collateral ignoring these pending fees.. This vulnerability stems from the fact that intent orders are guaranteed to execute (invalidation = 0), but the associated trading fees are not applied to the user's collateral until after the epoch price is committed. This creates a window where a user can open and close a position via intent order...

Vulnerable code:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;

import { Accumulator6 } from "@equilibria/root/accumulator/types/Accumulator6.sol";
import { UFixed6, UFixed6Lib } from "@equilibria/root/number/types/UFixed6.sol";
import { Fixed6, Fixed6Lib } from "@equilibria/root/number/types/Fixed6.sol";
import { IMarket } from "../interfaces/IMarket.sol";
import { Position } from "../types/Position.sol";
import { Order } from "../types/Order.sol";
import { Version } from "../types/Version.sol";
import { Checkpoint } from "../types/Checkpoint.sol";
import { Guarantee } from "../types/Guarantee.sol";

struct CheckpointAccumulationResponse {
    /// @dev Total Collateral change due to collateral, price override, and trade fee and offset
    Fixed6 collateral;

    /// @dev Liquidation fee accumulated for this checkpoint (only if the order is protected)
    UFixed6 liquidationFee;

    /// @dev Subtractive fee accumulated from the previous position to the next position (this amount is included in the linear fee)
    UFixed6 subtractiveFee;

    /// @dev Solver fee accumulated the previous position to the next position (this amount is included in the linear fee)
    UFixed6 solverFee;
}

struct CheckpointAccumulationResult {
    /// @dev Total Collateral change due to pnl, funding, and interest from the previous position to the next position
    Fixed6 collateral;

    /// @dev Collateral change from the difference between the price override and underlying market price
    Fixed6 priceOverride;

    /// @dev Trade fee accumulated for this checkpoint
    UFixed6 tradeFee;

    /// @dev Trade price impact accumulated for this checkpoint
    Fixed6 offset;

    /// @dev Settlement fee charged for this checkpoint
    UFixed6 settlementFee;

    /// @dev Liquidation fee accumulated for this checkpoint (only if the order is protected)
    UFixed6 liquidationFee;

    /// @dev Subtractive fee accumulated from the previous position to the next position (this amount is included in the linear fee)
    UFixed6 subtractiveFee;

    /// @dev Solver fee accumulated the previous position to the next position (this amount is included in the linear fee)
    UFixed6 solverFee;
}

/// @title CheckpointLib
/// @dev (external-safe): this library is safe to externalize
/// @notice Manages the logic for the local order accumulation
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));
    }

    /// @notice Converts the accumulation result into a response
    /// @param result The accumulation result
    /// @return response The accumulation response
    function _response(
        CheckpointAccumulationResult memory result
    ) private pure returns (CheckpointAccumulationResponse memory response) {
        response.collateral = result.collateral
            .add(result.priceOverride)
            .sub(Fixed6Lib.from(result.tradeFee))
            .sub(result.offset)
            .sub(Fixed6Lib.from(result.settlementFee));
        response.liquidationFee = result.liquidationFee;
        response.subtractiveFee = result.subtractiveFee;
        response.solverFee = result.solverFee;
    }

    /// @notice Accumulate pnl, funding, and interest from the latest position to next position
    /// @param fromPosition The previous latest position
    /// @param fromVersion The previous latest version
    /// @param toVersion The next version
    function _accumulateCollateral(
        Position memory fromPosition,
        Version memory fromVersion,
        Version memory toVersion
    ) private pure returns (Fixed6) {
        return toVersion.makerValue.accumulated(fromVersion.makerValue, fromPosition.maker)
            .add(toVersion.longValue.accumulated(fromVersion.longValue, fromPosition.long))
            .add(toVersion.shortValue.accumulated(fromVersion.shortValue, fromPosition.short));
    }

    /// @notice Accumulate trade fees for the next position
    /// @param order The next order
    /// @param guarantee The next guarantee
    /// @param toVersion The next version
    function _accumulateFee(
        Order memory order,
        Guarantee memory guarantee,
        Version memory toVersion
    ) private pure returns (UFixed6 tradeFee, UFixed6 subtractiveFee, UFixed6 solverFee) {
        UFixed6 takerTotal = order.takerTotal().sub(guarantee.takerFee);

        // accumulate total trade fees on maker and taker orders
        UFixed6 makerFee = Fixed6Lib.ZERO
            .sub(toVersion.makerFee.accumulated(Accumulator6(Fixed6Lib.ZERO), order.makerTotal()))
            .abs();
        UFixed6 takerFee = Fixed6Lib.ZERO
            .sub(toVersion.takerFee.accumulated(Accumulator6(Fixed6Lib.ZERO), takerTotal))
            .abs();

        // compute portion of trade fees that are subtractive
        UFixed6 makerSubtractiveFee = order.makerTotal().isZero() ?
            UFixed6Lib.ZERO :
            makerFee.muldiv(order.makerReferral, order.makerTotal());
        UFixed6 takerSubtractiveFee = takerTotal.isZero() ?
            UFixed6Lib.ZERO :
            takerFee.muldiv(order.takerReferral, takerTotal);

        // compute portion of subtractive fees that are solver fees
        solverFee = takerTotal.isZero() ?
            UFixed6Lib.ZERO :
            takerFee.muldiv(guarantee.solverReferral, takerTotal); // guarantee.solverReferral is instantiated as a subset of order.takerReferral

        tradeFee = makerFee.add(takerFee);
        subtractiveFee = makerSubtractiveFee.add(takerSubtractiveFee).sub(solverFee);
    }

    /// @notice Accumulate price offset for the next position
    /// @dev This includes adjustment for linear, proportional, and adiabatic order fees
    /// @param order The next order
    /// @param guarantee The next guarantee
    /// @param toVersion The next version
    function _accumulateOffset(
        Order memory order,
        Guarantee memory guarantee,
        Version memory toVersion
    ) private pure returns (Fixed6) {
        (UFixed6 takerPos, UFixed6 takerNeg) =
            (order.takerPos().sub(guarantee.takerPos()), order.takerNeg().sub(guarantee.takerNeg()));

        return Fixed6Lib.ZERO
            .sub(toVersion.makerOffset.accumulated(Accumulator6(Fixed6Lib.ZERO), order.makerTotal()))
            .sub(toVersion.takerPosOffset.accumulated(Accumulator6(Fixed6Lib.ZERO), takerPos))
            .sub(toVersion.takerNegOffset.accumulated(Accumulator6(Fixed6Lib.ZERO), takerNeg));
    }


    /// @notice Accumulate settlement fees for the next position
    /// @param order The next order
    /// @param guarantee The next guarantee
    /// @param toVersion The next version
    function _accumulateSettlementFee(
        Order memory order,
        Guarantee memory guarantee,
        Version memory toVersion
    ) private pure returns (UFixed6) {
        uint256 orders = order.orders - guarantee.orders;

        return toVersion.settlementFee.accumulated(Accumulator6(Fixed6Lib.ZERO), UFixed6Lib.from(orders)).abs();
    }

    /// @notice Accumulate liquidation fees for the next position
    /// @param order The next order
    /// @param toVersion The next version
    function _accumulateLiquidationFee(
        Order memory order,
        Version memory toVersion
    ) private pure returns (UFixed6) {
        if (!order.protected()) return UFixed6Lib.ZERO;
        return toVersion.liquidationFee.accumulated(Accumulator6(Fixed6Lib.ZERO), UFixed6Lib.ONE).abs();
    }

    /// @notice Accumulate price override pnl for the next position
    /// @param guarantee The next guarantee
    /// @param toVersion The next version
    function _accumulatePriceOverride(
        Guarantee memory guarantee,
        Version memory toVersion
    ) private pure returns (Fixed6) {
        return guarantee.priceAdjustment(toVersion.price);
    }
}

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;

import { Fixed6Lib } from "@equilibria/root/number/types/Fixed6.sol";
import { IMarket } from "../interfaces/IMarket.sol";
import { PositionLib } from "../types/Position.sol";
import { Order } from "../types/Order.sol";
import { Guarantee } from "../types/Guarantee.sol";

/// @title InvariantLib
/// @dev (external-safe): this library is safe to externalize
/// @notice Manages the logic for the system invariant
library InvariantLib {
    /// @notice Verifies the invariant of the market
    /// @param context The context to use
    /// @param updateContext The update context to use
    /// @param newOrder The order to verify the invariant for
    /// @param newGuarantee The guarantee to verify the invariant for
    function validate(
        IMarket.Context memory context,
        IMarket.UpdateContext memory updateContext,
        Order memory newOrder,
        Guarantee memory newGuarantee
    ) external {
        // emit created event first due to early return
        emit IMarket.OrderCreated(
            context.account,
            newOrder,
            newGuarantee,
            updateContext.liquidator,
            updateContext.orderReferrer,
            updateContext.guaranteeReferrer
        );

        if (
            context.pendingLocal.invalidation != 0 &&                              // pending orders are partially invalidatable
            context.pendingLocal.neg().gt(context.latestPositionLocal.magnitude()) // total pending close is greater than latest position
        ) revert IMarket.MarketOverCloseError();

        if (newOrder.protected() && !_validateProtection(context, newOrder))
            revert IMarket.MarketInvalidProtectionError();

        if (
            !(context.latestPositionLocal.magnitude().isZero() && context.pendingLocal.isEmpty()) &&    // sender has no position
            !(newOrder.isEmpty() && newOrder.collateral.gte(Fixed6Lib.ZERO)) &&                         // sender is depositing zero or more into account, without position change
            (
                !context.latestOracleVersion.valid ||
                context.currentTimestamp - context.latestOracleVersion.timestamp >= context.riskParameter.staleAfter
            )                                                                                           // price is not stale
        ) revert IMarket.MarketStalePriceError();

        if (context.marketParameter.closed && newOrder.increasesPosition())
            revert IMarket.MarketClosedError();

        if (
            updateContext.currentPositionGlobal.maker.gt(context.riskParameter.makerLimit) &&
            newOrder.increasesMaker()
        ) revert IMarket.MarketMakerOverLimitError();

        if (!updateContext.currentPositionLocal.singleSided()) revert IMarket.MarketNotSingleSidedError();

        if (
            (!context.latestPositionLocal.maker.isZero() && !updateContext.currentPositionLocal.skew().isZero()) ||
            (!context.latestPositionLocal.skew().isZero() && !updateContext.currentPositionLocal.maker.isZero())
        ) revert IMarket.MarketNotSingleSidedError();

        if (context.pendingLocal.invalidation != 0 && context.pendingLocal.crossesZero())
            revert IMarket.MarketNotSingleSidedError();

        if (newGuarantee.priceDeviation(context.latestOracleVersion.price).gt(context.marketParameter.maxPriceDeviation))
            revert IMarket.MarketIntentPriceDeviationError();

        if (newOrder.protected()) return; // The following invariants do not apply to protected position updates (liquidations)

        if (
            !updateContext.signer &&                                            // sender is relaying the account's signed intention
            !updateContext.operator &&                                          // sender is operator approved for account
            !(newOrder.isEmpty() && newOrder.collateral.gte(Fixed6Lib.ZERO))    // sender is depositing zero or more into account, without position change
        ) revert IMarket.MarketOperatorNotAllowedError();

        if (
            context.global.currentId > context.global.latestId + context.marketParameter.maxPendingGlobal ||
            context.local.currentId > context.local.latestId + context.marketParameter.maxPendingLocal
        ) revert IMarket.MarketExceedsPendingIdLimitError();

        if (
            !PositionLib.margined(
                updateContext.currentPositionLocal.magnitude(),
                context.latestOracleVersion,
                context.riskParameter,
                updateContext.collateralization,
                context.local.collateral.add(newGuarantee.priceAdjustment(context.latestOracleVersion.price)) // apply price override adjustment from intent if present
            )
        ) revert IMarket.MarketInsufficientMarginError();

        if (
            context.pendingLocal.protected() && // total pending position is protected
            !newOrder.protected()               // protection did not occur in this order (semphore already handles double-protection case)
        ) revert IMarket.MarketProtectedError();

        if (
            newOrder.liquidityCheckApplicable(context.marketParameter) &&
            newOrder.decreasesEfficiency(updateContext.currentPositionGlobal) &&
            updateContext.currentPositionGlobal.efficiency().lt(context.riskParameter.efficiencyLimit)
        ) revert IMarket.MarketEfficiencyUnderLimitError();

        if (
            newOrder.liquidityCheckApplicable(context.marketParameter) &&
            updateContext.currentPositionGlobal.socialized() &&
            newOrder.decreasesLiquidity(updateContext.currentPositionGlobal)
        ) revert IMarket.MarketInsufficientLiquidityError();

        if (context.local.collateral.lt(Fixed6Lib.ZERO))
            revert IMarket.MarketInsufficientCollateralError();
    }

    function _validateProtection(IMarket.Context memory context, Order memory newOrder) private pure returns (bool) {
        if (context.pendingLocal.crossesZero()) {
            if (!newOrder.isEmpty()) return false; // pending zero-cross, liquidate (lock) with no-op order
        } else {
            if (!context.pendingLocal.neg().eq(context.latestPositionLocal.magnitude())) return false; // no pending zero-cross, liquidate with full close
        }

        if (context.latestPositionLocal.maintained(
            context.latestOracleVersion,
            context.riskParameter,
            context.local.collateral
        )) return false; // latest position is properly maintained

        if (!newOrder.collateral.eq(Fixed6Lib.ZERO)) return false; // the order is modifying collateral

        return true;
    }
}


Vulnerability type: CWE-691|CWE-841