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.
Off-Chain
Price monitoring services detect changes.
Auction framework coordinates liquidator bidding.
Bundler constructs a single transaction for the winning sequence of liquidations.
On-Chain
CentralRegistry unlocks the specific market for the current transaction.
MarketManager handles liquidation execution and validates configuration.
Dynamic Penalty System manages incentive and close factor via transient storage.
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
Oracle Update & Operation Collection: Price feed updates create liquidation opportunities. Liquidators generate signed UserOps containing bid amounts and execution instructions.
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
.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:
Price change detected off-chain.
Auction runs and selects winners.
Bundler submits a transaction that:
Unlocks the market in CentralRegistry.
Sets auction paramters + collateral unlock in MarketManager.
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:
Buffer Mechanism: A 10 basis point (0.1%) buffer gives auction transactions priority for liquidations; implemented in BPS as 9990.
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.
Benefits:
Captures liquidations from interest accrual.
Handles LST (Liquid Staking Token) yield accumulation.
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:
Liquidators submit bids indicating desired liquidation incentive (stored as BPS + incentive) and close factor (BPS).
During the auction transaction, the authorized caller:
Unlocks the market in CentralRegistry.
Calls
setTransientLiquidationConfig(cToken, incentive, closeFactor)
on MarketManager, which:Validates bounds against token min/max.
Packs collateral address, incentive, and close factor into one transient slot.
Liquidation executes using these transient values.
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);
}
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.
// 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
Market unlock: performed via CentralRegistry (transient, per-tx).
Collateral unlock + parameters: set in MarketManager (transient, per-tx).
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:
Bulk Processing: Can handle multiple liquidations in a single transaction.
Cached Pricing: Retrieves asset prices once per transaction rather than per liquidation.
Consolidated Transfers: Aggregates results and returns both seized shares and debt to repay into a single transfer.
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:
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:
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:
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
Push vs. Pull Model:
Atlas OEV requires a push-based oracle model where price updates trigger on-chain events.
Each price update can initiate liquidation opportunities.
Transaction Sequencing:
When an oracle update transaction lands on-chain, it may trigger liquidations.
Atlas integrates with the oracle update process to capture OEV (Optimal Extractable Value).
Oracle Compatibility:
Designed to work with any push-based oracle.
RedStone push feeds are the primary integration target.
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