Skip to main content

Collateral & Margin Model

Overview

Ollo uses an isolated margin model where each position has its own dedicated collateral. Margin is denominated in USDC (6 decimals externally, converted to 18 decimal WAD precision internally) and tracked per-account across two buckets: available and committed.

Why Isolated Margin?

Isolated margin means a loss in one market cannot drain margin from positions in other markets. Each position's margin is independent — if a USD/JPY position gets liquidated, it does not affect a EUR/USD position in the same account. This provides clear risk boundaries at the cost of capital efficiency.

Architecture

┌──────────────────────────────────────────────────────────────────┐
│ FxAccount │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ availableMargin │ │
│ │ • Free to use for new orders │ │
│ │ • Free to withdraw │ │
│ │ • Receives PnL on position close │ │
│ │ • Receives funding payments (when position is paid) │ │
│ └────────────────────────────┬──────────────────────────────┘ │
│ │ │
│ placeOrder() │ closePosition() │
│ (commit) │ (release + PnL) │
│ │ │
│ ┌────────────────────────────▼──────────────────────────────┐ │
│ │ committedMargin │ │
│ │ • Locked in open orders and positions │ │
│ │ • Cannot be withdrawn │ │
│ │ • Released on cancel, close, or liquidation │ │
│ │ • Reduced by funding payments (when position owes) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Position A (USD/JPY): margin = 1000e18 │
│ Position B (EUR/USD): margin = 500e18 │
│ ───────────────────────────────────── │
│ committedMargin = 1500e18 (sum of all position margins) │
└──────────────────────────────────────────────────────────────────┘

How It Works

Deposit

Users deposit USDC into their FxAccount. The FxEngine transfers USDC from the user to the FxAccount contract, then records the deposit:

// In FxEngine.depositMargin():
USDC.transferFrom(msg.sender, account.accountAddress, amount);
uint256 amountWad = Units.usdcToWad(amount); // 6 decimals → 18 decimals
IFxAccount(account.accountAddress).recordDeposit(amountWad);

// In FxAccount.recordDeposit():
availableMargin += amount;

Anyone can deposit to any account. Only the account owner can withdraw.

Withdrawal

Withdrawals pull from availableMargin only. Committed margin cannot be withdrawn:

// In FxAccount.withdrawMargin():
if (availableMargin < Units.usdcToWad(amount)) revert InsufficientMargin();
availableMargin -= Units.usdcToWad(amount);
USDC.transfer(recipient, amount);

Order Placement (Commit)

When an order is placed, margin moves from available to committed:

// In FxAccount.placeOrder():
if (availableMargin < collateralAmount) revert InsufficientMargin();
availableMargin -= collateralAmount;
committedMargin += collateralAmount;

The marginAllocated field on the order tracks how much margin backs that specific order.

Exception — closing orders: When closing a position (collateralAmount = 0, opposite side), no margin is committed. The existing position's margin covers the close.

Order Cancellation (Release)

When an order is cancelled, unfilled margin returns to available:

// In FxAccount.cancelOrder():
if (order.marginAllocated > order.marginUsed) {
marginReturned = order.marginAllocated - order.marginUsed;
committedMargin -= marginReturned;
availableMargin += marginReturned;
}

Fill Processing (Margin Allocation per Fill)

When an order is partially filled, margin is allocated proportionally to each fill:

// In FxAccount.handleFill():
if (isFullyFilled) {
// Last fill gets remaining margin (prevents rounding errors)
marginForFill = order.marginAllocated - order.marginUsed;
} else {
marginForFill = (order.marginAllocated * fillQuantity) / order.originalQuantity;
}
order.marginUsed += marginForFill;

This margin is then passed to _updatePositionFromFill() to back the new or expanded position.

PnL Realization

When a position is closed (fully or partially), PnL is calculated and applied to margin:

PnL Formula

// LONG: profit when price goes up
pnl = size * (exitPrice - entryPrice) / entryPrice

// SHORT: profit when price goes down
pnl = size * (entryPrice - exitPrice) / entryPrice

Margin Adjustment

function _realizePnL(int256 pnl, uint128 marginAmount) internal {
if (pnl > 0) {
// Profit: return margin + profit to available
availableMargin += uint256(pnl) + marginAmount;
committedMargin -= marginAmount;
} else {
uint256 loss = uint256(-pnl);
if (loss >= marginAmount) {
// Total loss: all margin consumed
committedMargin -= marginAmount;
} else {
// Partial loss: return remaining margin to available
availableMargin += marginAmount - loss;
committedMargin -= marginAmount;
}
}
}

Closing Order Margin

When a closing order fills, its own marginUsed (which is 0 for closes without new collateral) is returned to available since it doesn't back any position:

// After closing fill:
if (marginUsed > 0) {
availableMargin += marginUsed;
committedMargin -= marginUsed;
}

Position Flipping

When a fill's opposite-side quantity exceeds the current position size, the position flips direction:

  1. Close the existing position completely — realize PnL on the full close size
  2. Open a new position in the opposite direction with the remaining fill quantity
  3. Margin is split proportionally between the close portion and the flip portion
  4. The close portion's margin is returned to available
  5. The flip portion's margin backs the new position
uint128 flipMargin = (marginUsed * flipSize) / fillQuantity;
uint128 closeMargin = marginUsed - flipMargin;

// Return close portion
availableMargin += closeMargin;
committedMargin -= closeMargin;

// New position backed by flip portion
pos.margin = flipMargin;

Funding Impact on Margin

Funding payments affect margin before any position modification:

Position Owes Funding (positive fundingPayment)

if (fundingPayment > 0) {
uint256 payment = uint256(fundingPayment);
if (availableMargin >= payment) {
// Deduct from available first
availableMargin -= payment;
} else {
// Available insufficient — eat into committed
uint256 shortfall = payment - availableMargin;
availableMargin = 0;
if (committedMargin >= shortfall) {
pos.margin -= uint128(shortfall);
committedMargin -= shortfall;
} else {
// Position insolvent — must be liquidated
committedMargin = 0;
}
}
}

Deducting from committed margin reduces the position's margin ratio, potentially triggering liquidation.

Position Receives Funding (negative fundingPayment)

if (fundingPayment < 0) {
availableMargin += uint256(-fundingPayment);
}

Received funding goes to available margin, which can be withdrawn or used for new orders.

Precision Conversion

USDC uses 6 decimals externally; all internal margin tracking uses 18 decimals (WAD):

// USDC → WAD (6 → 18 decimals)
function usdcToWad(uint256 usdc) returns (uint256) {
return usdc * 1e12;
}

// WAD → USDC (18 → 6 decimals)
function wadToUsdc(uint256 wad) returns (uint256) {
return wad / 1e12;
}

Collateral amounts in placeOrder() are in 6-decimal USDC. FxEngine converts to WAD before passing to FxAccount:

uint128 collateralWad = uint128(Units.usdcToWad(collateralAmount_));

Contract Interface

FxEngine (User-Facing)

/// @notice Deposit USDC as margin
/// @dev Anyone can deposit to any account. Requires USDC approval.
/// @param accountId The account to deposit to
/// @param amount Amount of USDC (6 decimals)
function depositMargin(uint256 accountId, uint256 amount) external;

/// @notice Withdraw available margin
/// @dev Only account owner. Cannot withdraw committed margin.
/// @param accountId The account to withdraw from
/// @param amount Amount of USDC (6 decimals)
function withdrawMargin(uint256 accountId, uint256 amount) external;

FxAccount (Internal)

/// @notice Record a margin deposit (increases availableMargin)
function recordDeposit(uint256 amount) external onlyFxEngine;

/// @notice Withdraw margin (decreases availableMargin, transfers USDC)
function withdrawMargin(uint256 amount, address recipient) external onlyFxEngine;

/// @notice Get margin breakdown
function getMarginInfo() external view returns (uint256 available, uint256 committed);

/// @notice Get total account value
function getAccountValue() external view returns (uint256 totalValue, int256 unrealizedPnL);

Reader (View)

/// @notice Get margin info for an account
function getAccountMargin(uint256 accountId) external view returns (uint256 available, uint256 committed);

/// @notice Get aggregated account data including margin
function getAccountData(uint256 accountId) external view returns (AccountData memory);

Events

event MarginDeposited(uint256 indexed accountId, uint256 amount);
event MarginWithdrawn(uint256 indexed accountId, uint256 amount);
event PnLRealized(uint256 indexed accountId, bytes32 indexed marketId, int256 pnl);
event FundingSettled(uint256 indexed accountId, bytes32 indexed marketId, int256 fundingPayment, int256 newCumulativeFunding);

Margin Flow Summary

Deposit USDC ──────────► availableMargin

placeOrder() (commit)


committedMargin ──────► position.margin
│ │
cancelOrder() close/liquidate
(release unfilled) │
│ ▼
▼ _realizePnL()
availableMargin ◄──── profit + margin returned
│ loss deducted from margin

withdrawMargin()


USDC out