Skip to main content

Core Contracts

This page provides a detailed technical reference for each core contract in the Ollo Finance protocol. For a high-level view of how they interact, see Architecture Overview.

In the current closed beta, USDC is the only allowlisted collateral asset, so contract names, parameter descriptions, and settlement notes below are USDC-specific.


FxEngine

Source: FxEngine.sol
Inheritance: IFxEngine, IMarketMatchHandler, IEvents

The FxEngine is the protocol's top-level coordinator. All user-facing operations route through it, and all market callbacks terminate at it. It owns the canonical state for accounts, markets, and cumulative funding.

Immutables

NameTypeDescription
USDCERC20Collateral token (6 decimals)
clearinghouseIClearinghouseVirtual token mint/burn authority
accountFactoryIFxAccountFactoryDeploys FxAccount contracts

State

NameTypeDescription
nextAccountIduint256Auto-incrementing account ID counter (starts at 1)
accountsmapping(uint256 => AccountInfo)Account ID → {accountAddress, owner}
userAccountIdmapping(address => uint256)User EOA → account ID
accountToIdmapping(address => uint256)Account contract address → account ID
marketsmapping(bytes32 => MarketConfig)Market ID → full config (addresses, leverage, OI, funding)
marketIdByAddressmapping(address => bytes32)Market contract address → market ID
owneraddressProtocol admin
fundingRateOracleIFundingRateOracleOracle for per-market funding rates
insuranceFundRecipientaddressReceives liquidation profits
badDebtmapping(bytes32 => uint256)Accumulated bad debt per market

MarketConfig Structure

struct MarketConfig {
address market; // Market contract address
address baseFxToken; // e.g., fxUSD
address quoteFxToken; // e.g., fxJPY
uint16 maxLeverage; // e.g., 50 = 50x
uint16 maintenanceMarginBps; // e.g., 100 = 1%
uint16 liquidationFeeBps; // e.g., 50 = 0.5%
bool isActive; // Accepts new positions
uint128 openInterestLong; // Total long OI
uint128 openInterestShort; // Total short OI
int256 cumulativeFunding; // Running funding total (18 decimals)
uint256 lastFundingUpdate; // Timestamp of last update
}

Account Management

createAccount() → uint256 accountId

Deploys a new FxAccount via the factory, registers it in all lookup mappings, and authorizes it as a clearinghouse participant. Each address can own exactly one account. Calling createAccount for an address that already has one reverts with AccountAlreadyExists.

An overloaded variant createAccount(address accountOwner_) allows creating an account on behalf of another address. Note that this means any address can create an account for any other address, which blocks the target from creating their own.

Margin Operations

depositMargin(uint256 accountId, uint256 amount)

Transfers USDC from msg.sender directly to the account contract via transferFrom, then calls recordDeposit with the WAD-converted amount. Anyone can deposit into any account.

withdrawMargin(uint256 accountId, uint256 amount)

Only the account owner can withdraw. The account converts the USDC amount to WAD internally, checks against availableMargin, and transfers USDC to the caller.

Order Routing

placeOrder(...) → (bytes32 clientKey, uint64 orderId)

The main entry point for trading. Validates market existence, activity, and leverage limits. Before placing the order, it updates cumulative funding for the market and settles any pending funding on the account's position.

The collateral amount is converted from USDC (6 decimals) to WAD (18 decimals) before being passed to the account. The account handles margin accounting, token minting, and the actual Market.place() call.

placeOrders(O[] calldata orders_) is a batch variant that iterates and calls the same internal logic per order.

cancelOrder(uint256 accountId, bytes32 marketId, bytes32 clientKey)

Delegates to FxAccount.cancelOrder, which removes the order from the market, returns unfilled margin, and burns any unused minted tokens.

Fill Callback

onFill(FillParams calldata params) - onlyMarket

Called by the Market contract after a match. The engine updates cumulative funding, then routes the fill to both the maker and taker accounts via handleFill. The maker's side is inverted relative to the trade side (a BID trade makes the maker a SHORT, the taker a LONG).

Funding

updateCumulativeFunding(bytes32 marketId)

Reads the current hourly funding rate from the oracle, calculates the funding delta based on elapsed time, and accumulates it into config.cumulativeFunding. Skips if either side's OI is zero (no funding when there's no counterparty).

Liquidation

liquidate(uint256 accountId, bytes32 marketId)

Callable by anyone. Settles funding, fetches the mark price from the oracle, calculates unrealized PnL, and checks whether the margin ratio is below the maintenance margin. If liquidatable, it instructs the account to place a market close order through the order book. After the fill, it calculates the liquidator reward and insurance fund profit using LiquidationLib, transfers rewards, and records any bad debt.

Open Interest Tracking

updateOpenInterest(bytes32 marketId, SIDE side, int128 sizeDelta)

Called by FxAccount contracts during position updates. Verifies the caller is a registered account (via accountToId), then adjusts the per-side OI on the market config.


FxAccount

Source: FxAccount.sol
Inheritance: IFxAccount, IEvents

Each user's trading state is isolated in a dedicated FxAccount contract. It holds the account's collateral, which is USDC in the current implementation, manages margin pools, tracks positions and orders, handles fill callbacks, and manages the virtual token lifecycle.

Immutables

NameTypeDescription
fxEngineIFxEngineThe coordinating engine
clearinghouseIClearinghouseMint/burn authority
USDCERC20Collateral token
owneraddressAccount owner (EOA)
accountIduint256Unique identifier

Position Structure

struct Position {
uint128 size; // In index tokens
uint128 entryPrice; // Weighted average (18 decimals)
uint128 margin; // Collateral backing this position
IFxEngine.SIDE side; // LONG or SHORT
uint32 openBlock; // Block number at open
bool isOpen; // Active flag
int256 realizedPnL; // Accumulated realized PnL
int256 lastCumulativeFunding; // Cumulative funding at last settlement
}

Margin Pools

The account maintains two margin balances (both in WAD):

  • availableMargin: Free collateral. Source for new orders, funding payments, and withdrawals.
  • committedMargin: Locked against open positions and active orders. Released on close or cancellation.

Order Placement

placeOrder(...) → (bytes32 clientKey, uint64 orderId) - onlyFxEngine

Determines whether the order is opening or closing a position:

  • Opening: deducts collateralAmount from availableMargin, adds to committedMargin. Position size = collateralAmount × leverage. Mints virtual tokens via the clearinghouse.
  • Closing: detected when side != pos.side && collateralAmount == 0. No margin movement. Uses existing token balances (received when opening) to place the close order.

The order is pre-registered in ordersByKey with a generated clientKey (hash of address, nonce, timestamp) before calling Market.place(). This prevents a race condition where the fill callback arrives before the order is recorded.

Fill Handling

handleFill(...) - onlyFxEngine

Processes a fill event from the engine. Steps:

  1. Settle pending funding for the position.
  2. Update order tracking (filled quantity, active status).
  3. Calculate proportional margin for this fill.
  4. Call _updatePositionFromFill to modify position state.

Position Update Logic

_updatePositionFromFill handles four cases:

1. New position: No existing position. Sets all fields, updates OI.

2. Same-side increase: Adds to size, recalculates weighted average entry price:

totalCost = (oldSize × oldEntry / 1e18) + (fillQty × fillPrice / 1e18)
newEntry = (totalCost × 1e18) / newSize

3. Opposite-side full close or flip: If fillQuantity >= pos.size:

  • Calculates PnL on the closed portion.
  • Realizes PnL into margin.
  • Reduces OI for the closed side.
  • If fillQuantity > pos.size, the excess opens a new position in the opposite direction (flip). The margin is split proportionally between close and flip.
  • If exactly equal, deletes the position entirely.

4. Opposite-side partial close: fillQuantity < pos.size:

  • Calculates PnL on closed portion.
  • Reduces position size and margin proportionally.
  • Reduces OI.

PnL Calculation

// LONG: PnL = size × (exitPrice - entryPrice) / entryPrice
// SHORT: PnL = size × (entryPrice - exitPrice) / entryPrice

PnL is realized into margin via _realizePnL:

  • Profit: availableMargin += profit + marginAmount, committedMargin -= marginAmount
  • Loss ≤ margin: availableMargin += (marginAmount - loss), committedMargin -= marginAmount
  • Loss ≥ margin: total margin loss, committedMargin -= marginAmount

Funding Settlement

_settleFunding(marketId, currentCumulativeFunding) calculates the funding payment for a position:

delta = currentCumulativeFunding - pos.lastCumulativeFunding
payment = (size × delta) / 1e18

Direction depends on side: longs pay when payment > 0, shorts pay when payment < 0 (sign is flipped for shorts).

Payment deduction follows a waterfall: availableMargin first, then committedMargin (via pos.margin). If both are exhausted, the position is insolvent and must be liquidated.

Token Management

Opening (mint): For LONG orders, mints quote tokens (fxJPY) proportional to size × price. For SHORT orders, mints base tokens (fxUSD) equal to position size.

Closing (use existing): Uses tokens received during the open. For SHORT-at-loss scenarios (price moved against), the shortfall is minted. This extra debt is forgiven during clearing since the loss is already deducted from margin.

Clearing (_clearTokensAndDebt): After a full close, for each token type:

  1. Burn tokens against debt (min of balance and debt).
  2. Burn any surplus tokens (profit case: extra tokens from favorable fills).
  3. Forgive any remaining debt (loss case: loss already settled in margin).

Cancellation (_burnUnusedTokens): Burns token balances against outstanding debt, up to the lesser of balance and debt.

Liquidation Functions

placeLiquidationOrder - Places a market close order (opposite side of the position) through the order book. Generates a liquidation-specific client key.

finalizeLiquidation - Deducts total rewards from availableMargin and approves USDC transfer to the engine.

transferRewards - Transfers USDC to a specified address (liquidator or insurance fund).


Market

Source: Market.sol
Inheritance: IMarket, IEvents

The Market contract implements a central limit order book (CLOB) with deterministic price-time priority matching. It supports both limit and market orders for a single trading pair (numeraire/index).

Immutables

NameTypeDescription
matchHandleraddressReceives onFill callbacks (the FxEngine)
numeraireERC20Quote asset (e.g., fxJPY). Bids are denominated in numeraire.
indexERC20Base asset (e.g., fxUSD). Asks are denominated in index.

Key Constants

NameValueDescription
WAD1e18Fixed-point precision for price arithmetic

Data Structures

Price Type

type Price is uint128;

Prices are 18-decimal fixed-point values representing numeraire per index.

Order

struct Order {
uint64 id;
uint32 blocknumber;
address trader;
STATUS status; // NULL, OPEN, PARTIAL, FILLED, CANCELLED
KIND kind; // MARKET, LIMIT
MarketTypes.SIDE side; // BID, ASK
Price price;
uint128 quantity; // Original amount
uint128 remaining; // Unfilled amount
bytes32 clientKey; // Correlation key for upstream tracking
}

Level (Price Level)

struct Level {
uint128 bidDepth; // Total resting bid quantity (numeraire)
uint128 askDepth; // Total resting ask quantity (index)
Queue.T bids; // FIFO queue of bid order IDs
Queue.T asks; // FIFO queue of ask order IDs
}

Price-Time Priority Engine

The matching engine uses two complementary data structures:

Red-Black Trees (RedBlackTreeLib) maintain sorted price levels:

  • Bid tree: keys are stored as type(uint128).max - price to achieve descending order (highest bid first) using the tree's natural ascending traversal.
  • Ask tree: keys are stored as raw prices, naturally ascending (lowest ask first).

FIFO Queues (Queue) maintain time priority within each price level. Orders at the same price are matched in insertion order.

Ask Tree (ascending)         Bid Tree (descending via negation)
┌─────────┐ ┌─────────┐
│ 155.00 │ ← best ask │ 154.90 │ ← best bid
│ 155.10 │ │ 154.80 │
│ 155.25 │ │ 154.50 │
└─────────┘ └─────────┘

Each price level:
┌─────────────────────────────────────┐
│ Level @ 155.00 │
│ askDepth: 5000 (index tokens) │
│ asks: [order#7, order#12, order#19] │ ← FIFO
└─────────────────────────────────────┘

Order Types

Limit Bid (__placeBid)

  1. Transfer numeraire tokens from trader to contract.
  2. Walk the ask tree starting at best ask:
    • At each price level ≤ limit price, match against FIFO queue of asks.
    • For each ask: calculate maxIndexBuyable = (remainingNumeraire × WAD) / askPrice.
    • Fill fully or partially, transfer numeraire to ask maker, accumulate index received.
    • Fire __notifyFill callback per match.
  3. Transfer accumulated index tokens to bidder.
  4. If unfilled quantity remains, rest the order on the bid side at the limit price.
  5. Insert price level into bid tree if new.

Limit Ask (__placeAsk)

  1. Transfer index tokens from trader to contract.
  2. Walk the bid tree starting at best bid:
    • At each price level ≥ limit price, match against FIFO queue of bids.
    • For each bid: calculate maxIndexBuyable = (bidNumeraireRemaining × WAD) / bidPrice.
    • Fill fully or partially, transfer index to bid maker, accumulate numeraire received.
    • Fire __notifyFill callback per match.
  3. Transfer accumulated numeraire tokens to asker.
  4. If unfilled quantity remains, rest the order on the ask side at the limit price.
  5. Insert price level into ask tree if new.

Market Bid (__placeMarketBid)

Identical to limit bid but with no price limit. Sweeps all available ask levels. Reverts with InsufficientLiquidity if the ask book is empty at entry. Any unspent numeraire is returned to the trader.

Market Ask (__placeMarketAsk)

Identical to limit ask but with no price limit. Sweeps all available bid levels. Reverts with InsufficientLiquidity if the bid book is empty at entry. Any unsold index tokens are returned to the trader.

WAD Price Arithmetic

All price calculations use 18-decimal fixed-point arithmetic:

// Numeraire spent for a given index quantity at price:
numeraireSpent = (indexQuantity × price) / WAD

// Max index buyable for a given numeraire budget at price:
maxIndexBuyable = (numeraireBudget × WAD) / price

Bid orders are denominated in numeraire (how much quote currency to spend). Ask orders are denominated in index (how much base currency to sell). This asymmetry means bid depth at a level is measured in numeraire, ask depth in index.

Dust Handling

Rounding in WAD arithmetic can leave small residual amounts ("dust") in bid orders. The contract handles this by checking if the remaining numeraire can buy zero index at the limit price:

if ((uint256(remainingNumeraire) * WAD) / limitPrice == 0) {
bid.status = STATUS.FILLED;
}

For filled bids with remaining dust in the level depth, the dust is subtracted from bidDepth to keep the tree clean.

Order Lifecycle

StatusMeaning
NULLDefault / uninitialized
OPENResting on the book, no fills yet
PARTIALPartially filled, still resting
FILLEDFully filled, removed from queue
CANCELLEDRemoved by trader via remove()

Order Modification

remove(uint64 id_) - Cancels an open or partial limit order. Returns remaining tokens to the trader. Removes from the FIFO queue and cleans up the price level if empty.

reduce(uint64 id_, uint128 newRemaining_) - Reduces the remaining quantity of a resting limit order without changing its queue position. If reduced to zero, treated as cancellation.

Fill Callback

After each match, the contract calls __notifyFill on the matchHandler (FxEngine) with:

struct FillParams {
uint64 makerOrderId;
uint64 takerOrderId;
bytes32 makerClientKey;
bytes32 takerClientKey;
address maker; // trader address of maker order
address taker; // trader address of taker order
MarketTypes.SIDE side; // BID or ASK (taker's side)
uint128 fillPrice; // Execution price
uint128 indexQuantity; // Amount of index tokens exchanged
bool makerFullyFilled;
bool takerFullyFilled;
}

The callback is non-reverting by design. The protocol can handle callback failures without disrupting the matching engine.


Clearinghouse

Source: Clearinghouse.sol
Inheritance: IClearinghouse, IEvents

The Clearinghouse is the sole authority for minting and burning virtual FX tokens. It tracks a per-account, per-token debt ledger that enables the phantom token accounting model.

State

NameTypeDescription
fxEngineaddressThe FxEngine contract
owneraddressAdmin
isTokenRegisteredmapping(address => bool)Registered FX token whitelist
registeredTokensaddress[]Array of all registered tokens
debtmapping(address => mapping(address => uint256))account → token → debt amount
authorizedClearinghousesmapping(address => bool)Accounts authorized to mint/burn

Access Control

  • onlyOwner: Token registration, engine configuration.
  • onlyFxEngine: authorizeClearinghouse - only the engine can grant/revoke clearinghouse privileges.
  • onlyAuthorized: mint, burn, burnSurplus, forgiveDebt - only authorized accounts (FxAccounts).

Operations

mint(address token, address to, uint256 amount)

Mints amount of token to to and records the debt against msg.sender:

debt[msg.sender][token] += amount;
IFxToken(token).mint(to, amount);

The debt is tracked against the caller (the FxAccount), not the recipient. This means the account that requested the mint is responsible for clearing the debt.

burn(address token, address from, uint256 amount)

Burns tokens from from and reduces the caller's debt. Reverts if debt[msg.sender][token] < amount (cannot burn more than owed) or if the target has insufficient balance.

burnSurplus(address token, address from, uint256 amount)

Burns tokens without reducing debt. Used when a position closes in profit. The account holds more tokens than it has debt for. The surplus represents the profit side of the trade, which has already been realized in margin.

forgiveDebt(address token, uint256 amount)

Reduces debt without burning tokens. Used when a position closes at a loss. The account's debt exceeds its token balance because the loss was already deducted from margin via _realizePnL.

Debt Invariant

For any account with an open position, the relationship between token balance and debt reflects the PnL state:

ScenarioToken Balance vs. DebtClearing Action
Breakevenbalance == debtburn(balance)
Profitbalance > debtburn(debt) + burnSurplus(balance - debt)
Lossbalance < debtburn(balance) + forgiveDebt(debt - balance)

FundingRateOracle

Source: FundingRateOracle.sol
Inheritance: IFundingRateOracle, IEvents

Calculates funding rates from externally pushed prices and onchain open interest data. The funding rate determines periodic payments between longs and shorts to keep the perpetual price anchored to the index.

Formula

r = α × premium + β × skew

Where:

  • premium = (P_perp - P_index) / P_index - deviation of perpetual price from index
  • skew = (OI_long - OI_short) / (OI_long + OI_short) - imbalance between long and short OI
  • α (alpha) = weight on the premium component (default: 0.0001e18)
  • β (beta) = weight on the skew component (default: 0.00005e18)

A positive rate means longs pay shorts. A negative rate means shorts pay longs.

Configuration

ParameterDefaultDescription
defaultAlpha0.0001e18Global premium weight
defaultBeta0.00005e18Global skew weight
maxPriceAge5 minutesStaleness threshold for price data
maxFundingRate0 (disabled)Maximum rate magnitude. Set to 0 to disable bounds.

Per-market overrides (alphaOverride, betaOverride) take precedence over defaults. Setting an override to 0 falls back to the global default.

Roles

Price Updaters - External services that push perp and index prices at regular intervals via pushPrices or batchPushPrices. Authorized via setAuthorizedPriceUpdater.

Keepers - Bots that trigger onchain funding rate calculation via updateFundingRate or batchUpdateFundingRates. Typically called hourly. Authorized via setAuthorizedKeeper.

Rate Calculation Flow

  1. Keeper calls updateFundingRate(marketId).
  2. Contract checks price staleness against maxPriceAge. Reverts with StalePriceData if stale.
  3. Fetches OI from the Reader contract (which reads from FxEngine's market config).
  4. Computes premium and skew components.
  5. Applies r = (α × premium) / 1e18 + (β × skew) / 1e18.
  6. Clamps to [-maxFundingRate, +maxFundingRate] if bounds are set.
  7. Stores result in fundingData[marketId].

View Functions

FunctionReturnsDescription
getFundingRate(marketId)int256Last calculated rate
getPrices(marketId)(uint128, uint128, uint256)Last pushed perp price, index price, timestamp
isPriceStale(marketId)(bool, uint256)Whether data is stale + age in seconds
previewFundingRate(marketId)(int256, int256, int256)Preview rate, premium, and skew without updating
getMarketFundingData(marketId)7 valuesFull snapshot: prices, ages, rate, alpha, beta