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
| Name | Type | Description |
|---|---|---|
USDC | ERC20 | Collateral token (6 decimals) |
clearinghouse | IClearinghouse | Virtual token mint/burn authority |
accountFactory | IFxAccountFactory | Deploys FxAccount contracts |
State
| Name | Type | Description |
|---|---|---|
nextAccountId | uint256 | Auto-incrementing account ID counter (starts at 1) |
accounts | mapping(uint256 => AccountInfo) | Account ID → {accountAddress, owner} |
userAccountId | mapping(address => uint256) | User EOA → account ID |
accountToId | mapping(address => uint256) | Account contract address → account ID |
markets | mapping(bytes32 => MarketConfig) | Market ID → full config (addresses, leverage, OI, funding) |
marketIdByAddress | mapping(address => bytes32) | Market contract address → market ID |
owner | address | Protocol admin |
fundingRateOracle | IFundingRateOracle | Oracle for per-market funding rates |
insuranceFundRecipient | address | Receives liquidation profits |
badDebt | mapping(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
| Name | Type | Description |
|---|---|---|
fxEngine | IFxEngine | The coordinating engine |
clearinghouse | IClearinghouse | Mint/burn authority |
USDC | ERC20 | Collateral token |
owner | address | Account owner (EOA) |
accountId | uint256 | Unique 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
collateralAmountfromavailableMargin, adds tocommittedMargin. 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:
- Settle pending funding for the position.
- Update order tracking (filled quantity, active status).
- Calculate proportional margin for this fill.
- Call
_updatePositionFromFillto 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:
- Burn tokens against debt (min of balance and debt).
- Burn any surplus tokens (profit case: extra tokens from favorable fills).
- 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
| Name | Type | Description |
|---|---|---|
matchHandler | address | Receives onFill callbacks (the FxEngine) |
numeraire | ERC20 | Quote asset (e.g., fxJPY). Bids are denominated in numeraire. |
index | ERC20 | Base asset (e.g., fxUSD). Asks are denominated in index. |
Key Constants
| Name | Value | Description |
|---|---|---|
WAD | 1e18 | Fixed-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 - priceto 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)
- Transfer numeraire tokens from trader to contract.
- 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
__notifyFillcallback per match.
- Transfer accumulated index tokens to bidder.
- If unfilled quantity remains, rest the order on the bid side at the limit price.
- Insert price level into bid tree if new.
Limit Ask (__placeAsk)
- Transfer index tokens from trader to contract.
- 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
__notifyFillcallback per match.
- Transfer accumulated numeraire tokens to asker.
- If unfilled quantity remains, rest the order on the ask side at the limit price.
- 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
| Status | Meaning |
|---|---|
NULL | Default / uninitialized |
OPEN | Resting on the book, no fills yet |
PARTIAL | Partially filled, still resting |
FILLED | Fully filled, removed from queue |
CANCELLED | Removed 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
| Name | Type | Description |
|---|---|---|
fxEngine | address | The FxEngine contract |
owner | address | Admin |
isTokenRegistered | mapping(address => bool) | Registered FX token whitelist |
registeredTokens | address[] | Array of all registered tokens |
debt | mapping(address => mapping(address => uint256)) | account → token → debt amount |
authorizedClearinghouses | mapping(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:
| Scenario | Token Balance vs. Debt | Clearing Action |
|---|---|---|
| Breakeven | balance == debt | burn(balance) |
| Profit | balance > debt | burn(debt) + burnSurplus(balance - debt) |
| Loss | balance < debt | burn(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
| Parameter | Default | Description |
|---|---|---|
defaultAlpha | 0.0001e18 | Global premium weight |
defaultBeta | 0.00005e18 | Global skew weight |
maxPriceAge | 5 minutes | Staleness threshold for price data |
maxFundingRate | 0 (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
- Keeper calls
updateFundingRate(marketId). - Contract checks price staleness against
maxPriceAge. Reverts withStalePriceDataif stale. - Fetches OI from the
Readercontract (which reads from FxEngine's market config). - Computes premium and skew components.
- Applies
r = (α × premium) / 1e18 + (β × skew) / 1e18. - Clamps to
[-maxFundingRate, +maxFundingRate]if bounds are set. - Stores result in
fundingData[marketId].
View Functions
| Function | Returns | Description |
|---|---|---|
getFundingRate(marketId) | int256 | Last 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 values | Full snapshot: prices, ages, rate, alpha, beta |