Orderflow Auction System

Overview

The Curvance orderflow auction system combines off-chain liquidation auctions with on-chain enforcement to enable:

  • Dynamic liquidation penalty and close factor per auction.

  • Priority access within a small solvency window via auction buffer.

  • Efficient batch liquidations with cached pricing.

  • Strict permissions and correctness guards via transient storage.

On-chain logic refers to this as 'auction-based liquidations' and integrates cleanly with any off-chain solver/auction stack (e.g. Atlas/Fastlane).

System Architecture

The Curvance orderflow auction system integrates on-chain and off-chain components to enable efficient liquidations with dynamic liquidation sizes and penalty rates.

  1. Off-Chain

    1. Price monitoring services detect changes.

    2. Auction framework coordinates liquidator bidding.

    3. Bundler constructs a single transaction for the winning sequence of liquidations.

  2. On-Chain

    1. CentralRegistry unlocks the specific market for the current transaction.

    2. MarketManager handles liquidation execution and validates configuration.

    3. Dynamic Penalty System manages incentive and close factor via transient storage.

    4. Auction Buffer System provides priority access to liquidations.

These components work together to process liquidations with optimal penalty rates while maximizing value capture and minimizing bad debt risk.

Key Features

  • Dynamic Liquidation Penalties: Adjustable within per-token min/max ranges, set per-transaction via transient storage.

  • Transient Storage: Parameters apply only within the current transaction then reset.

  • Oracle Integration: Dual-oracle pathing with an isolated pair fetch price udring liquidation to prioritize risk mitigation.

  • Liquidation Buffer: 10 bps advantage for auction transactions.

  • Efficient Batch Liquidations: Single config/pricing fetch used across multiple liquidations in one transaction.

Auction workflow

  1. Oracle Update & Operation Collection: Price feed updates create liquidation opportunities. Liquidators generate signed UserOps containing bid amounts and execution instructions.

  2. Auction & Bundling: Auctioneer receives bids from liquidators in the form of signed AA (Account Abstraction) operations. The auctioneer then selects highest bidding operations and bundles them into a single atomic transaction with itself as tx.origin.

  3. Transaction Execution: The bundled transaction executes on the Auctioneer Entrypoint contract, which:

    • Performs pre-execution balance checks.

    • Calls each liquidator's operation in descending bid order.

    • Verifies bid payment through post-execution balance checks.

    • Continues to the next highest bidder if a liquidator fails to pay.

This mechanism maximizes MEV capture while ensuring liquidations always complete in a timely manner.

Core Dataflows

Price Update to Liquidation Flow

The journey from price change to liquidation execution follows these steps:

  1. Price change detected off-chain.

  2. Auction runs and selects winners.

  3. Bundler submits a transaction that:

    1. Unlocks the market in CentralRegistry.

    2. Sets auction paramters + collateral unlock in MarketManager.

    3. Executes batch liquidations.

This auction typically completes within milliseconds off-chain before submitting the transaction.

Liquidation Buffer System

The Auction buffer system provides priority access to liquidations:

  1. Buffer Mechanism: A 10 basis point (0.1%) buffer gives auction transactions priority for liquidations; implemented in BPS as 9990.

  2. Implementation: When Auction transactions are detected via transient storage, liquidation health checks apply the buffer and shifts borderline positions into liquidable range in a narrow window.

  3. Benefits:

    1. Captures liquidations from interest accrual.

    2. Handles LST (Liquid Staking Token) yield accumulation.

    3. Compensates for network latency.

    /// @notice Buffer to ensure orderflow auction-based liquidations have
    ///         priority versus basic liquidations, in `BPS`.
    /// @dev 9990 = 99.9%. Multiplied then divided by `BPS` = 10 bps buffer.
    uint256 public constant AUCTION_BUFFER = 9990;
   function _checkLiquidationConfig(
        address cToken
    ) internal view returns (uint256, uint256, uint256) {
        (address unlockedCToken, uint256 liqIncentive, uint256 closeFactor) =
            getTransientLiquidationConfig();

        bool unlockedMarket = centralRegistry.isMarketUnlocked();
        bool unlockedCollateral = unlockedCToken == cToken;

        if (unlockedMarket || unlockedCollateral) {
            // This is an attempted auction liquidation, and is configured
            // correctly so give them the auction priority buffer.
            if (unlockedMarket && unlockedCollateral) {
                return (AUCTION_BUFFER, liqIncentive, closeFactor);
            }

            // This is an attempted auction liquidation, but its misconfigured
            // and unauthorized because of this.
            revert MarketManager__UnauthorizedLiquidation();
        }

        // This is not an attempted auction liquidation, so approve the
        // liquidation, but without any offchain liquidation config values.
        return (0, 0, 0);
    }

Dynamic Penalty Dataflow

The dynamic penalty mechanism operates as follows:

  1. Liquidators submit bids indicating desired liquidation incentive (stored as BPS + incentive) and close factor (BPS).

  2. During the auction transaction, the authorized caller:

    1. Unlocks the market in CentralRegistry.

    2. Calls setTransientLiquidationConfig(cToken, incentive, closeFactor) on MarketManager, which:

      1. Validates bounds against token min/max.

      2. Packs collateral address, incentive, and close factor into one transient slot.

  3. Liquidation executes using these transient values.

  4. After the transaction, parameters reset automatically; they can also be explicitly reset mid-bundle if needed.

Per-transaction auction parameters are packed into a single transient word:

  • [0..159] unlocked collateral cToken address.

  • [160..175] liquidation incentive (BPS, stored as BPS + incentive).

  • [176..191] close factor (BPS).

uint256 internal constant _TRANSIENT_LIQUIDATION_CONFIG_KEY
    = 0x1966ec4daf81281b2aba49348128e9b155301b8486bde131e0db16a52b730b82;
uint256 internal constant _BITMASK_COLLATERAL_UNLOCKED = (1 << 160) - 1;
uint256 internal constant _BITPOS_LIQ_INCENTIVE = 160;
uint256 internal constant _BITPOS_CLOSE_FACTOR = 176;
    function setTransientLiquidationConfig(
        address cToken,
        uint256 incentive,
        uint256 closeFactor
    ) external {
        _checkAuctionPermissions();
        _checkIsListedToken(cToken);

         CurvanceToken memory c = _tokenConfig[cToken];

        // Make sure this token actually can be liquidated, by being
        // collateralizable in the first place.
        if (c.collRatio == 0) {
            revert MarketManager__UnauthorizedLiquidation();
        }

        // Validate `incentive` is within configured incentive bounds. This
        // also validates `incentive` is not > the 16 bits we have allocated.
        if (incentive < c.liqIncMin || incentive > c.liqIncMax) {
            _revert(_INVALID_PARAMETER_SELECTOR);
        }

        // Validate `closeFactor` is within configured allowed close factor
        // range. This also validates `closeFactor` is not > the 16 bits we
        // have allocated.
        if (closeFactor < c.closeFactorMin || closeFactor > c.closeFactorMax) {
            _revert(_INVALID_PARAMETER_SELECTOR);
        }

        uint256 liqConfig = uint256(uint160(cToken));
        assembly {
            // Mask `liqConfig` to the lower 160 bits, in case the upper bits
            // somehow are not clean.
            liqConfig := and(liqConfig, _BITMASK_COLLATERAL_UNLOCKED)
            // Equals: liqConfig | (incentive << _BITPOS_LIQ_INCENTIVE) |
            //         closeFactor << _BITPOS_CLOSE_FACTOR.
            liqConfig := or(
                liqConfig,
                or(
                    shl(_BITPOS_LIQ_INCENTIVE, incentive),
                    shl(_BITPOS_CLOSE_FACTOR, closeFactor)
                )  
            )

            tstore(_TRANSIENT_LIQUIDATION_CONFIG_KEY, liqConfig)
        }
    }
    function resetTransientLiquidationConfig() external {
        _checkAuctionPermissions();

        /// @solidity memory-safe-assembly
        assembly {
            tstore(_TRANSIENT_LIQUIDATION_CONFIG_KEY, 0)
        }
    }
    function getTransientLiquidationConfig() public view returns (
        address cTokenUnlocked,
        uint256 incentive,
        uint256 closeFactor
    ) {
        uint256 liqConfig;
        /// @solidity memory-safe-assembly
        assembly {
            liqConfig := tload(_TRANSIENT_LIQUIDATION_CONFIG_KEY)
        }

        cTokenUnlocked = address(uint160(liqConfig));
        incentive = uint16(liqConfig >> _BITPOS_LIQ_INCENTIVE);
        closeFactor = uint16(liqConfig >> _BITPOS_CLOSE_FACTOR);
    }

Important unit notes:

  • Liquidation incentive is represented as BPS + incentive (e.g. 10500 -> 5% incentive). Transient incentive must use this representation to pass range checks.

  • Close factor is in BPS (0-10000).

Fallback Mechanism

When auction parameters are not set (transient values are zero), the system uses the base liquidation incentive an interpolated close factor configured by governance, computed from the account's lFactor.

Note: No 10 bps buffer is applied in non-auction liquidations.

// In _getLiquidationConfig() inside of MarketManagerIsolated.sol
// fallback to base curve when transient is zero.

        // We only need to cache these variables if we did not receive close
        // factor/liquidation incentive from `getLiquidationConfig`.
        if (aData.closeFactor == 0 || aData.liqInc == 0) {
            aData.closeFactorBase = c.closeFactorBase;
            aData.closeFactorCurve = c.closeFactorCurve;
            aData.liqIncBase = c.liqIncBase;
            aData.liqIncCurve = c.liqIncCurve;
        }
// In _canLiquidate() inside of MarketManagerIsolated.sol 
      
        // If this liquidation does not have offchain submitted
        // parameters then closeFactorCurve will not be 0. We know this since
        // closeFactorCurve is BPS - closeFactorBase and closeFactorBase is
        // limited to MAX_BASE_CFACTOR meaning closeFactorCurve cannot ever be
        // 0 unless we did not receive offchain parameters and we need to
        // calculate close factor and liquidation penalty onchain.
        if (aData.closeFactorCurve != 0) {
            aData.closeFactor = aData.closeFactorBase +
                _mulDiv(aData.closeFactorCurve, aData.lFactor, WAD);
            aData.liqInc = aData.liqIncBase +
                _mulDiv(aData.liqIncCurve, aData.lFactor, WAD);
        }

Market and Collateral Unlocking Mechanism

  1. Market unlock: performed via CentralRegistry (transient, per-tx).

  2. Collateral unlock + parameters: set in MarketManager (transient, per-tx).

  3. Both must be set; otherwise, auction attempts revert as unauthorized.

    function unlockAuctionForMarket(address marketToUnlock) external {
        if (!hasAuctionPermissions[msg.sender]) {
            revert CentralRegistry__Unauthorized();
        }

        // Validate that you're unlocking an approved market manager.
        if (!isMarketManager[marketToUnlock]) {
            revert CentralRegistry__InvalidParameter();
        }

        uint256 marketToUnlockUint = uint256(uint160(marketToUnlock));
        /// @solidity memory-safe-assembly
        assembly {
            tstore(_TRANSIENT_MARKET_UNLOCKED_KEY, marketToUnlockUint)
        }
    }
    function isMarketUnlocked() public view returns (bool isUnlocked) {
        uint256 result;
        /// @solidity memory-safe-assembly
        assembly {
            result := tload(_TRANSIENT_MARKET_UNLOCKED_KEY)
        }

        // CASE: This is not an Auction tx, so allow all markets,
        // and return false, the caller is not approved for auction-based
        // liquidations. 
        if (result == 0) {
            return isUnlocked;
        }

        // True if the caller is approved for auction-based liquidations,
        // otherwise false.
        isUnlocked = uint256(uint160(msg.sender)) == result;
    }

Batch Liquidation Support.

The system supports efficient multi-liquidation processing:

  1. Bulk Processing: Can handle multiple liquidations in a single transaction.

  2. Cached Pricing: Retrieves asset prices once per transaction rather than per liquidation.

  3. Consolidated Transfers: Aggregates results and returns both seized shares and debt to repay into a single transfer.

  4. Gas Optimization: Significantly reduces gas costs during liquidation cascades.

This design allows for processing thousands of liquidations in a single transaction, improving efficiency during market stress.

        // In canLiquidate inside of MarketManagerIsolated.sol
        
        (TokenLiqData memory tData, AccountLiqData memory aData) =
            _getLiquidationConfig(action.collateralToken, action.debtToken);

        // Amounts array is empty since the max amount possible
        // will be liquidated.
        result.liquidatedShares = new uint256[](action.numAccounts);
        address cachedAccount;
        address priorAccount;
        for (uint256 i; i < action.numAccounts; ++i) {
        // ...rest of the function...

Auction Mechanics

Atlas Auction Structure

The Atlas auction system is the core mechanism for capturing liquidation MEV in Curvance. When a liquidation opportunity arises, the system conducts a brief (typically 300ms) auction before submitting an on-chain transaction.

Bid Components

Each liquidation bid consists of these key elements:

  • Solver Address: The liquidator's smart contract that will execute the liquidation.

  • Execution Data: Calldata specifying which account to liquidate and method parameters.

  • Penalty Bid: The liquidation penalty rate the liquidator is willing to accept.

  • Close Factor Bid: The percentage of debt the liquidator wants to liquidate.

  • OEV Bid Amount: Payment offered to the protocol and other stakeholders.

  • Signature: Cryptographic verification of the bid's authenticity.

Bid Classification

The auction system automatically classifies bids into two distinct categories:

Minimum Penalty Bids:

  • Uses the protocol-defined minimum penalty (e.g., 5%).

  • Must include a positive OEV payment amount.

  • Typically used during normal market conditions.

Higher Penalty Bids:

  • Specifies a penalty above the minimum (e.g., 6-20%).

  • No OEV payment required.

  • Used during high volatility or significant slippage.

Bid Ranking Algorithm

The auction employs a specialized ranking system with these core rules:

Primary Ranking by Penalty Tier:

  • Minimum penalty bids compete exclusively against other minimum penalty bids.

  • Higher penalty bids compete based on the penalty rate offered.

Secondary Ranking Rules:

  • For minimum penalty bids: Higher OEV payment receives priority.

  • For higher penalty bids: Lower penalty rate receives priority.

Tertiary Sorting: When OEV payments or penalty rates are identical, bids are ordered by timestamp.

Ranking Examples

Example 1: Minimum Penalty Competition

Consider these bids at the minimum 5% penalty:

Liquidator
Penalty
OEV Payment
Timestamp

LiquidatorA

5%

10 ETH

10:00:01

LiquidatorB

5%

8 ETH

10:00:00

LiquidatorC

5%

12 ETH

10:00:02

Ranking order: LiquidatorC → LiquidatorA → LiquidatorB (sorted by highest OEV payment).

Example 2: Higher Penalty Competition

Consider these bids with penalties above minimum:

Liquidator
Penalty
OEV Payment
Timestamp

LiquidatorA

7%

0

10:00:01

LiquidatorB

8%

0

10:00:00

LiquidatorC

6%

0

10:00:02

Ranking order: LiquidatorC → LiquidatorA → LiquidatorB (sorted by lowest penalty).

Example 3: Mixed Competition

When both minimum and higher penalty bids exist:

Liquidator
Penalty
OEV Payment
Timestamp

LiquidatorA

5%

10 ETH

10:00:01

LiquidatorB

7%

0

10:00:00

LiquidatorC

5%

12 ETH

10:00:02

Ranking will attempt LiquidatorC first (highest OEV at min penalty), then LiquidatorA (second highest OEV at min penalty), then LiquidatorB (lowest penalty among higher penalty bids).

Execution Flow

The Atlas transaction processes these bids sequentially:

  • The highest-ranked bid's solver contract is called first.

  • If execution succeeds, the transaction completes and value is distributed.

  • If execution fails, the next-ranked bid is attempted.

  • This continues until either a liquidation succeeds.

For Minimum Penalty Bids with OEV:

As specified in the DAppControl contract, the allocateValue hook distributes the OEV payment between:

  • The bundler (who submits the transaction).

  • Oracle (price feed provider).

  • Fastlane (Operations Relay infrastructure).

  • The Curvance treasury.

  • The exact distribution percentages are configured by Curvance governance.

  • OEV bids are only present when a solver wins with a penalty bid equal to the minimum penalty.

For Higher Penalty Bids:

  • No OEV payment is included or distributed.

  • All excess value from the higher penalty accrues to the liquidator.

  • No value distribution to protocol or infrastructure stakeholders occurs.

This dual economic model aligns incentives effectively: during normal market conditions, the protocol captures value through OEV payments, while during stressed market conditions, liquidators retain more value to offset increased risk and ensure liquidations complete successfully.

Oracle Data Flow

  1. Push vs. Pull Model:

    1. Atlas OEV requires a push-based oracle model where price updates trigger on-chain events.

    2. Each price update can initiate liquidation opportunities.

  2. Transaction Sequencing:

    1. When an oracle update transaction lands on-chain, it may trigger liquidations.

    2. Atlas integrates with the oracle update process to capture OEV (Optimal Extractable Value).

  3. Oracle Compatibility:

    1. Designed to work with any push-based oracle.

    2. RedStone push feeds are the primary integration target.

    3. Support for LST (Liquid Staking Token) redemption rate oracles.

Data Storage Model

The system employs two distinct storage layers:

Permanent Storage:

  • Penalty ranges (min, max, default values).

  • Account health statuses.

  • System configuration parameters.

Transient Storage:

  • Dynamic penalty values during Atlas transactions.

  • Close factor values.

  • Collateral unlock status.

  • Values automatically reset after transaction completion.

This separation ensures critical data persists while dynamic values remain ephemeral.


Resources:

Atlas Docs: https://atlas-docs.pages.dev/atlas/introduction

Last updated