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:
- Cumulative Funding: FxEngine maintains a running total (
cumulativeFunding) per market - Position Checkpoint: Each position stores
lastCumulativeFundingat time of last settlement - 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
| Parameter | Default | Description |
|---|---|---|
defaultAlpha | 0.0001e18 (0.01%) | Premium weight in funding formula |
defaultBeta | 0.00005e18 (0.005%) | Skew weight in funding formula |
maxPriceAge | 300 (5 minutes) | Staleness threshold for price data |
maxFundingRate | 0 (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
- Float Service pushes latest perp and index prices via
pushPrices() - Keeper Bot calls
updateFundingRate()to calculate new rate - Oracle fetches OI data from
Reader.getMarket() - Rate stored in
fundingData[marketId]
Position Interaction
- User calls
placeOrder()on FxEngine - FxEngine calls
updateCumulativeFunding()to sync market funding - FxEngine calls
settleFundingExternal()on FxAccount - FxAccount calculates and applies funding payment to margin
- Position's
lastCumulativeFundingupdated to current value - Order execution proceeds
Edge Cases
No Open Interest
When either openInterestLong or openInterestShort is zero:
- Funding accumulation is skipped
lastFundingUpdateis 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:
- All
availableMarginis consumed first - Remaining shortfall deducted from
committedMargin(position margin) - Position margin ratio decreases, potentially triggering liquidation
- If position becomes fully insolvent (
committedMargin = 0), it must be liquidated
Stale Price Data
If block.timestamp - lastUpdate > maxPriceAge:
updateFundingRate()reverts withStalePriceData- Existing rate remains in effect until fresh prices arrive
- Cumulative funding continues to accumulate using the last valid rate
Access Control
| Function | Access |
|---|---|
pushPrices / batchPushPrices | Authorized price updaters only |
updateFundingRate / batchUpdateFundingRates | Authorized keepers only |
setDefaultParams / setMarketParams | Owner only |
setReader | Owner only |
setMaxPriceAge / setMaxFundingRate | Owner only |
setAuthorizedPriceUpdater / setAuthorizedKeeper | Owner only |
transferOwnership | Owner only |
updateCumulativeFunding (FxEngine) | Permissionless |
Keeper Setup
To set up the funding rate system:
- Deploy FundingRateOracle
- Call
setReader(readerAddress)to connect to Reader - Call
setAuthorizedPriceUpdater(keeperAddress, true)to authorize the float service - Call
setAuthorizedKeeper(keeperAddress, true)to authorize rate calculation - Call
FxEngine.setFundingRateOracle(oracleAddress)to connect to FxEngine - Float service calls
pushPrices()at regular intervals (e.g., every minute) - Keeper calls
updateFundingRate()hourly to calculate and store the rate