Skip to main content

Pricing Mechanism — Funding Rate

Overview

The funding rate mechanism keeps perpetual contract prices aligned with the underlying spot price. When the perpetual price trades above the index price, longs pay shorts; when below, shorts pay longs. This creates an economic incentive for arbitrageurs to close the gap between the two prices.

Architecture

┌─────────────────────┐      pushPrices()      ┌──────────────────┐
│ Float Service │ ────────────────────── │ FundingRateOracle│
│ (Off-chain Keeper) │ │ │
└─────────────────────┘ │ • priceData │
│ • fundingData │
┌─────────────────────┐ updateFundingRate() │ • α, β params │
│ Keeper Bot │ ────────────────────── │ │
│ │ └────────┬─────────┘
└─────────────────────┘ │
│ getMarket() (for OI)

┌─────────────────────┐
│ Reader │
│ • getMarket() │
│ • getMarkPrice() │
└────────┬────────────┘

│ getFundingRate()

┌─────────────────────┐ updateCumulativeFunding() ┌─────────────────────┐
│ FxEngine │ ◄──────────────────────────── │ FxEngine │
│ │ │ • cumulativeFunding │
│ Position Updates │ settleFundingExternal() │ • lastFundingUpdate │
│ Order Placement │ ─────────────────────────────►│ │
│ Liquidations │ └─────────────────────┘
└─────────────────────┘

│ _settleFunding()

┌──────────────────────────┐
│ FxAccount │
│ • availableMargin │
│ • lastCumulativeFunding │
└──────────────────────────┘

Funding Rate Calculation

Formula

r = α · premium + β · skew

Where:

  • r: Funding rate (18 decimals, per hour)
  • α: Premium weight (default: 0.0001e18 = 0.01%)
  • β: Skew weight (default: 0.00005e18 = 0.005%)
  • premium: Price deviation from index
  • skew: Open interest imbalance

Premium Component

premium = (P_perp - P_index) / P_index
  • Positive when perp trades above spot → longs pay
  • Negative when perp trades below spot → shorts pay

Skew Component

skew = (OI_long - OI_short) / (OI_long + OI_short)
  • Range: [-1, +1]
  • Positive when more longs → additional pressure for longs to pay
  • Zero when OI is balanced or empty

Rate Bounds

The maxFundingRate parameter can limit extreme rates. When set to a non-zero value, the calculated rate is clamped to [-maxFundingRate, +maxFundingRate]. By default this is 0 (no limit).

Settlement Mechanism

Lazy Settlement

Funding is not settled continuously on-chain. Instead, it uses a lazy settlement pattern where funding is calculated and applied only when positions are modified:

  1. Cumulative Funding: FxEngine maintains a running total (cumulativeFunding) per market
  2. Position Checkpoint: Each position stores lastCumulativeFunding at time of last settlement
  3. Settlement Trigger: Funding is settled before any position modification

Settlement Triggers

Funding settlement occurs automatically before:

  • Opening a new position
  • Adding to an existing position
  • Closing a position (full or partial)
  • Liquidation

Funding Payment Calculation

fundingDelta = currentCumulativeFunding - position.lastCumulativeFunding

// For LONG positions:
fundingPayment = (positionSize * fundingDelta) / PRICE_PRECISION

// For SHORT positions:
fundingPayment = -(positionSize * fundingDelta) / PRICE_PRECISION
  • Positive fundingPayment: Position owes funding (deducted from margin)
  • Negative fundingPayment: Position receives funding (added to margin)

Time-Weighted Accumulation

When cumulative funding is updated:

timeElapsed = block.timestamp - lastFundingUpdate
fundingDelta = (fundingRate * timeElapsed) / SECONDS_PER_HOUR
cumulativeFunding += fundingDelta

This ensures positions are charged proportionally to time held, regardless of when settlement occurs.

Contract Interfaces

FundingRateOracle

Price Updates (Float Service)

/// @notice Push price data for a market
/// @dev Called by authorized float service at regular intervals
function pushPrices(bytes32 marketId, uint128 perpPrice, uint128 indexPrice) external onlyPriceUpdater;

/// @notice Batch push prices for multiple markets
function batchPushPrices(
bytes32[] calldata marketIds,
uint128[] calldata perpPrices,
uint128[] calldata indexPrices
) external onlyPriceUpdater;

Rate Calculation (Keeper)

/// @notice Calculate and update funding rate for a market
/// @dev Called by keeper bot (typically hourly)
/// @dev Uses Reader.getMarket() to fetch OI data
/// @dev Reverts with StalePriceData if prices are too old
function updateFundingRate(bytes32 marketId) external onlyKeeper;

/// @notice Batch update funding rates for multiple markets
function batchUpdateFundingRates(bytes32[] calldata marketIds) external onlyKeeper;

Admin Functions

/// @notice Transfer ownership of the oracle
function transferOwnership(address newOwner) external onlyOwner;

/// @notice Update Reader reference
function setReader(address _reader) external onlyOwner;

/// @notice Authorize or revoke a price updater (float service)
function setAuthorizedPriceUpdater(address updater, bool authorized) external onlyOwner;

/// @notice Authorize or revoke a keeper
function setAuthorizedKeeper(address keeper, bool authorized) external onlyOwner;

/// @notice Set global default alpha and beta
function setDefaultParams(int256 _alpha, int256 _beta) external onlyOwner;

/// @notice Set per-market alpha and beta overrides (0 = use default)
function setMarketParams(bytes32 marketId, int256 _alpha, int256 _beta) external onlyOwner;

/// @notice Set maximum price age for staleness check
function setMaxPriceAge(uint256 _maxPriceAge) external onlyOwner;

/// @notice Set maximum funding rate magnitude (0 = no limit)
function setMaxFundingRate(int256 _maxFundingRate) external onlyOwner;

View Functions

/// @notice Get current funding rate for a market (18 decimals, per hour)
function getFundingRate(bytes32 marketId) external view returns (int256 rate);

/// @notice Get the last pushed prices for a market
function getPrices(bytes32 marketId)
external view returns (uint128 perpPrice, uint128 indexPrice, uint256 lastUpdate);

/// @notice Preview funding rate without updating state
function previewFundingRate(bytes32 marketId)
external view returns (int256 rate, int256 premium, int256 skew);

/// @notice Check if price data is stale
function isPriceStale(bytes32 marketId) external view returns (bool isStale, uint256 age);

/// @notice Get full market funding data in one call
function getMarketFundingData(bytes32 marketId)
external view returns (
uint128 perpPrice, uint128 indexPrice, uint256 priceAge,
int256 fundingRate, uint256 rateAge, int256 alpha, int256 beta
);

FxEngine Functions

/// @notice Update cumulative funding for a market (permissionless)
/// @dev Called internally before position modifications
/// @dev Skips if no OI exists on either side
function updateCumulativeFunding(bytes32 marketId) public;

FxAccount Functions

/// @notice Settle funding for a position (external entry point)
/// @dev Called by FxEngine before position modifications
function settleFundingExternal(bytes32 marketId, int256 currentCumulativeFunding)
external onlyFxEngine returns (int256 fundingPayment);

/// @notice Calculate pending funding payment without settling
function getPendingFunding(bytes32 marketId, int256 currentCumulativeFunding)
external view returns (int256 fundingPayment);

Reader Functions

/// @notice Get pending funding delta if updated now
function getPendingFunding(bytes32 marketId) external view returns (int256);

/// @notice Get current cumulative funding
function getCumulativeFunding(bytes32 marketId) external view returns (int256);

/// @notice Get current funding rate from oracle
function getCurrentFundingRate(bytes32 marketId) external view returns (int256);

Configuration Parameters

Global Parameters

ParameterDefaultDescription
defaultAlpha0.0001e18 (0.01%)Premium weight in funding formula
defaultBeta0.00005e18 (0.005%)Skew weight in funding formula
maxPriceAge300 (5 minutes)Staleness threshold for price data
maxFundingRate0 (no limit)Maximum funding rate magnitude

Per-Market Overrides

Markets can have custom α and β values via setMarketParams(). A value of 0 indicates use of global default.

Events

// FundingRateOracle
event PricesUpdated(bytes32 indexed marketId, uint128 perpPrice, uint128 indexPrice, uint256 timestamp);
event FundingRateCalculated(bytes32 indexed marketId, int256 rate, int256 premium, int256 skew, uint256 timestamp);
event ReaderUpdated(address indexed oldReader, address indexed newReader);
event PriceUpdaterAuthorized(address indexed updater, bool authorized);
event KeeperAuthorized(address indexed keeper, bool authorized);
event DefaultParamsUpdated(int256 alpha, int256 beta);
event MarketParamsUpdated(bytes32 indexed marketId, int256 alpha, int256 beta);
event MaxPriceAgeUpdated(uint256 oldMaxAge, uint256 newMaxAge);
event MaxFundingRateUpdated(int256 oldMaxRate, int256 newMaxRate);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

// FxEngine
event CumulativeFundingUpdated(bytes32 indexed marketId, int256 cumulativeFunding, int256 fundingDelta, uint256 timestamp);

// FxAccount
event FundingSettled(uint256 indexed accountId, bytes32 indexed marketId, int256 fundingPayment, int256 newCumulativeFunding);

Errors

error Unauthorized();
error ZeroAddress();
error StalePriceData(bytes32 marketId, uint256 lastUpdate, uint256 maxAge);
error InvalidPrice();
error FundingOracleNotSet(); // FxEngine: oracle not configured

Operational Flow

Hourly Update Cycle

  1. Float Service pushes latest perp and index prices via pushPrices()
  2. Keeper Bot calls updateFundingRate() to calculate new rate
  3. Oracle fetches OI data from Reader.getMarket()
  4. Rate stored in fundingData[marketId]

Position Interaction

  1. User calls placeOrder() on FxEngine
  2. FxEngine calls updateCumulativeFunding() to sync market funding
  3. FxEngine calls settleFundingExternal() on FxAccount
  4. FxAccount calculates and applies funding payment to margin
  5. Position's lastCumulativeFunding updated to current value
  6. Order execution proceeds

Edge Cases

No Open Interest

When either openInterestLong or openInterestShort is zero:

  • Funding accumulation is skipped
  • lastFundingUpdate is still updated to current timestamp
  • No counterparty exists to receive/pay funding

Insufficient Margin for Funding

When a position owes more funding than available margin:

  1. All availableMargin is consumed first
  2. Remaining shortfall deducted from committedMargin (position margin)
  3. Position margin ratio decreases, potentially triggering liquidation
  4. If position becomes fully insolvent (committedMargin = 0), it must be liquidated

Stale Price Data

If block.timestamp - lastUpdate > maxPriceAge:

  • updateFundingRate() reverts with StalePriceData
  • Existing rate remains in effect until fresh prices arrive
  • Cumulative funding continues to accumulate using the last valid rate

Access Control

FunctionAccess
pushPrices / batchPushPricesAuthorized price updaters only
updateFundingRate / batchUpdateFundingRatesAuthorized keepers only
setDefaultParams / setMarketParamsOwner only
setReaderOwner only
setMaxPriceAge / setMaxFundingRateOwner only
setAuthorizedPriceUpdater / setAuthorizedKeeperOwner only
transferOwnershipOwner only
updateCumulativeFunding (FxEngine)Permissionless

Keeper Setup

To set up the funding rate system:

  1. Deploy FundingRateOracle
  2. Call setReader(readerAddress) to connect to Reader
  3. Call setAuthorizedPriceUpdater(keeperAddress, true) to authorize the float service
  4. Call setAuthorizedKeeper(keeperAddress, true) to authorize rate calculation
  5. Call FxEngine.setFundingRateOracle(oracleAddress) to connect to FxEngine
  6. Float service calls pushPrices() at regular intervals (e.g., every minute)
  7. Keeper calls updateFundingRate() hourly to calculate and store the rate