Smart contract documentation

0. Overview

This document describes how to interact with Pingu’s smart-contract protocol directly through JSON-RPC calls (eth_call, eth_sendTransaction).

It covers all read and write functions needed to build integrations, indexers, trading bots, and dashboards.

The protocol is composed of multiple on-chain modules, each responsible for a specific part of the system (orders, positions, risk, markets, funding).

This section summarizes all contracts, conventions, and units used throughout the documentation.


0.1 Contract Map

Below is the core set of contracts exposed to integrators:

Contract
Responsibility

Orders

Submit, cancel, and batch-manage trading orders.

OrderStore

Persistent storage for all orders.

Positions

PnL / funding fee helpers for positions.

PositionStore

Persistent storage for user positions + open interest tracking.

MarketStore

Immutable and mutable market configuration (fees, leverage, oracles, funding parameters…).

RiskStore

Global risk parameters (maxDelta, maxOI, max position size).

Funding

Funding rate logic (EMA, clamping, real-time extrapolation).

FundingStore

Stored funding trackers, intervals, EMA values, and timestamps.

Note: Contract addresses are fetched on-chain through the DataStore mapping.

Integrators should query the canonical addresses from DataStore instead of hardcoding them.


0.2 JSON-RPC Basics

The following RPC methods are used throughout:

Write operations:

  • eth_sendTransaction:

    Used to submit orders, cancel orders, or perform any state-changing action.

Read operations:

  • eth_call:

    Used to read:

    • orders

    • positions

    • markets

    • risk parameters

    • funding trackers

    • open interest

Logs / Events:

  • eth_getLogs:

    Useful for:

    • detecting order submissions/executions

    • monitoring cancellations

    • tracking position updates


0.3 Data Units & Conventions

To avoid ambiguity, the protocol follows a consistent set of units:

Numbers

  • All monetary values (size, margin, price) use 18 decimals by default.

  • If the collateral token has fewer decimals (e.g. USDC = 6), amounts must be scaled accordingly before encoding calldata.

Prices

  • Always expressed in 18-decimals fixed-point format, regardless of the quoted asset.

Fees & Rates

  • Trading fees, funding factors, and thresholds are expressed in basis points (bps):

    • 1% = 100 bps

    • 100% = 10,000 bps

Funding Rates

  • Funding rates are stored and returned as bps × 1e18.

  • Funding tracker increments are also 18-decimal fixed-point values.


0.4 Time Conventions

The protocol uses the following timestamps:

  • block.timestamp — standard EVM time

  • expiry — custom TTL for an order

  • fundingInterval — default 8 hours

  • fundingStore.lastUpdated(asset, market) — last time funding was checkpointed

Several operations rely on funding intervals, not block numbers.

Funding only accrues when at least one full interval has passed.


A typical integration uses the modules in this order:

  1. Read market configuration (MarketStore.get)

  2. Fetch user positions (PositionStore.getPosition / getUserPositions)

  3. Check risk limits (RiskStore.checkMaxOI, checkMaxDelta, etc. via eth_call)

  4. Submit order (Orders.submitOrder)

  5. Track execution via eth_getLogs (OrderCreated, OrderExecuted)

  6. Compute PnL using Positions.getPnL or replicating the logic off-chain

  7. Monitor funding (Funding.getRealTimeFundingTracker)


1. Submitting an Order

(Orders.sol + OrderStore.sol)

1.1 Order Structure

Contract: OrderStore

Every order in the protocol follows the OrderStore.Order struct:

Parameter
Type
Description

orderId

uint256

Incremental ID. Ignored when submitting.

user

address

User that submitted the order. Overridden by the protocol (msg.sender).

asset

address

Collateral token address (ex: USDC), or address(0) for native asset.

market

string

Market symbol (ex: "ETH-USD").

margin

uint256

Collateral allocated to the order (in wei).

size

uint256

Notional size = margin × leverage (in wei).

price

uint256

Trigger/protection price for limit/stop orders. Ignored for market orders.

fee

uint256

Fee paid. Ignored when submitting (computed internally).

isLong

bool

true for long, false for short.

orderType

uint8

0 = market, 1 = limit, 2 = stop.

isReduceOnly

bool

Whether this order can only reduce an existing position.

timestamp

uint256

Submission timestamp. Overridden by the protocol.

expiry

uint256

Custom TTL. 0 means “no explicit expiration”.

cancelOrderId

uint256

Optional: cancel this order when the submitted order executes (OCO logic).

Integrator note:

When submitting, you only set:

asset, market, margin, size, price, isLong, orderType, isReduceOnly, expiry, cancelOrderId.

Everything else is ignored and overwritten by the protocol.


1.2 The submitOrder Function

Contract: Orders

function submitOrder(
    OrderStore.Order memory params,
    uint256 tpPrice,
    uint256 slPrice
) external payable;

Key Rules

  • If tpPrice > 0 or slPrice > 0:

    → The main order will always be isReduceOnly = false

    TP/SL orders created automatically will be isReduceOnly = true.

  • Limit/stop rules:

    • orderType = 1 or 2 requires price > 0.

    • For short orders (isLong = false), using the current market price P:

      • If price < P → this is a stop entry (breakdown) → orderType = 2.

      • If price > P → this is a limit entry (bounce / pullback) → orderType = 1.

    • For long orders (isLong = true):

      • If price > P → this is a stop entry (breakout) → orderType = 2.

      • If price < P → this is a limit entry (buy the dip) → orderType = 1.

⚠️ Important The orderType must be consistent with the side (isLong) and the relation between price and the current market price. If you submit a “stop” (orderType = 2) at a level that should be a limit (or the opposite), the order will be immediately executable and can be filled right away as if it were a market order.

  • Expiry rules:

    • expiry = 0 → no custom TTL

    • Otherwise must respect per-market max TTL.

  • Collateral & fees:

    • If asset == address(0) (native):

      • msg.value must include margin + fee

    • If ERC20:

      • User must approve FundStore for at least margin + fee

      • msg.value is ignored (and refunded if present)


1.3 TP/SL Behavior

tpPrice and slPrice are optional trigger prices.

Validation logic:

  • For long:

    • tpPrice > entryPrice

    • slPrice < entryPrice

  • For short:

    • tpPrice < entryPrice

    • slPrice > entryPrice

If both TP and SL are provided:

  • Long: tpPrice > slPrice

  • Short: tpPrice < slPrice

The protocol automatically creates up to 2 additional reduce-only orders (TP & SL) and connects them via OCO (cancel one when the other is executed).


1.4 RPC Example — Submitting an Order

RPC Transaction Template

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "id": 1,
  "params": [
    {
      "from": "<USER_ADDRESS>",
      "to": "<ORDERS_CONTRACT_ADDRESS>",
      "value": "0x<margin_plus_fee_if_native_asset>",
      "data": "0x<encoded_submitOrder_calldata>",
      "gas": "0x<estimate>",
      "gasPrice": "0x<gas_price>"
    }
  ]
}

Encoding Example (ethers.js)

const iface = new ethers.utils.Interface(OrdersAbi);

const params = {
  orderId: 0,
  user: ethers.constants.AddressZero,
  asset: USDC,
  market: "ETH-USD",
  margin: ethers.utils.parseUnits("100", 6),
  size: ethers.utils.parseUnits("500", 6),
  price: 0,
  fee: 0,
  isLong: true,
  orderType: 0,
  isReduceOnly: false,
  timestamp: 0,
  expiry: 0,
  cancelOrderId: 0
};

const tpPrice = ethers.utils.parseUnits("4000", 18);
const slPrice = ethers.utils.parseUnits("3200", 18);

const data = iface.encodeFunctionData("submitOrder", [params, tpPrice, slPrice]);

1.5 Getting the Submitted Order ID

Event emitted:

event OrderCreated(
    uint256 indexed orderId,
    address indexed user,
    address indexed asset,
    string market,
    uint256 margin,
    uint256 size,
    uint256 price,
    uint256 fee,
    bool isLong,
    uint8 orderType,
    bool isReduceOnly,
    uint256 expiry,
    uint256 cancelOrderId
);

You can filter logs via eth_getLogs using:

  • event signature hash

  • user as indexed topic

Option 2 — Query the store

OrderStore exposes:

function oid() external view returns (uint256);       // last used ID
function get(uint256 orderId) external view returns (Order memory);

Workflow:

  1. Send the tx

  2. After confirmation, call oid()

  3. Query get(orderId)

2. Cancel & Batch Orders

(Orders.sol + OrderStore.sol)

2.1 Overview

There are three main flows:

  1. User-initiated cancellations:

    • cancelOrder(uint256 orderId)

    • cancelOrders(uint256[] orderIds)

  2. Protocol/infra-initiated cancellations (keepers, processor, etc.):

    • cancelOrder(uint256 orderId, string reason)

    • cancelOrders(uint256[] orderIds, string[] reasons)

  3. Batch submit + optional cancel in one transaction:

    • submitSimpleOrders(Order[] params, uint256[] orderIdsToCancel)

All of them funnel through the same internal helper:

function _cancelOrder(uint256 orderId, string memory reason) internal;

which refunds margin+fee (for non-reduce-only orders) and emits OrderCancelled.


2.2 User-Initiated Cancellation

2.2.1 cancelOrder

Contract: Orders

function cancelOrder(uint256 orderId) external ifNotPaused;

Behavior:

  • Loads the order from OrderStore.

  • Checks:

    • order.size > 0 → the order exists.

    • order.user == msg.sender → only the owner can cancel.

  • Calls _cancelOrder(orderId, "by-user").

RPC example (single order):

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "id": 1,
  "params": [
    {
      "from": "<USER_ADDRESS>",
      "to": "<ORDERS_CONTRACT_ADDRESS>",
      "data": "0x<encodeFunctionData('cancelOrder', [orderId])>",
      "value": "0x0"
    }
  ]
}

2.2.2 cancelOrders

Contract: Orders

function cancelOrders(uint256[] calldata orderIds) external ifNotPaused;

Behavior:

  • Iterates through orderIds.

  • For each:

    • Fetches the order from OrderStore.

    • If order.size > 0 and order.user == msg.sender, calls:

      _cancelOrder(orderIds[i], "by-user");
      
    • Otherwise it silently skips the ID.

This is the preferred way to cancel multiple user orders in a single tx.

RPC example (batch):

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "id": 1,
  "params": [
    {
      "from": "<USER_ADDRESS>",
      "to": "<ORDERS_CONTRACT_ADDRESS>",
      "data": "0x<encodeFunctionData('cancelOrders', [[123, 456, 789]])>",
      "value": "0x0"
    }
  ]
}

2.3 Protocol / Infra Cancellation

These functions are restricted by onlyContract (internal role system).

They are meant for processor / keeper / infra contracts, not for end-users.

2.3.1 cancelOrder (with reason)

function cancelOrder(
    uint256 orderId,
    string calldata reason
) external onlyContract;

Typical reasons: "insufficient-margin", "stale-price", "risk-limit", etc.

2.3.2 cancelOrders (batch with reasons)

function cancelOrders(
    uint256[] calldata orderIds,
    string[] calldata reasons
) external onlyContract;
  • Both arrays must have the same length.

  • Each orderIds[i] is cancelled with reasons[i].


2.4 Internal Cancellation Logic

All paths end up in:

function _cancelOrder(uint256 orderId, string memory reason) internal {
    OrderStore.Order memory order = orderStore.get(orderId);
    if (order.size == 0) return;

    orderStore.remove(orderId);

    if (!order.isReduceOnly) {
        fundStore.transferOut(
            order.asset,
            order.user,
            order.margin + order.fee
        );
    }

    emit OrderCancelled(orderId, order.user, reason);
}
  • If the order is reduce-only (TP/SL, etc.):

    • No margin was locked for this order → no refund.

  • If the order is not reduce-only:

    • User gets back margin + fee from FundStore.

  • An OrderCancelled event is emitted with:

    • orderId

    • user

    • reason


2.5 Batch Submit + Optional Cancel: submitSimpleOrders

This is the “one-shot” flow: cancel some orders, then submit new ones in the same tx.

Contract: Orders

function submitSimpleOrders(
    OrderStore.Order[] calldata params,
    uint256[] calldata orderIdsToCancel
) external payable ifNotPaused;

Behavior

  1. Cancel phase

    for (uint256 i = 0; i < orderIdsToCancel.length; i++) {
        OrderStore.Order memory orderToCancel = orderStore.get(orderIdsToCancel[i]);
        if (orderToCancel.size > 0 && orderToCancel.user == msg.sender) {
            _cancelOrder(orderIdsToCancel[i], "by-user");
        }
    }
    
    • Same checks as cancelOrders:

      only cancels if the order exists and belongs to msg.sender.

  2. Submit phase

    uint256 totalValueConsumed;
    for (uint256 i = 0; i < params.length; i++) {
        uint256 valueConsumed;
        (, valueConsumed) = _submitOrder(params[i]);
        totalValueConsumed += valueConsumed;
    }
    
    • Calls _submitOrder for each params[i].

    • valueConsumed is:

      • margin + fee if collateral is native (e.g. ETH),

      • 0 if collateral is ERC20 (because tokens are pulled via transferIn).

  3. Refund excess ETH

    if (msg.value > totalValueConsumed) {
        payable(msg.sender).sendValue(msg.value - totalValueConsumed);
    }
    
    • If the user over-sent native value (for multiple orders), the contract refunds the difference.

RPC Example (conceptual)

Submit 2 new orders and cancel 1 previous order:

{
  "jsonrpc": "2.0",
  "method": "eth_sendTransaction",
  "id": 1,
  "params": [
    {
      "from": "<USER_ADDRESS>",
      "to": "<ORDERS_CONTRACT_ADDRESS>",
      "value": "0x<sum_of_native_margin_plus_fees_or_0_for_erc20>",
      "data": "0x<encodeFunctionData('submitSimpleOrders', [ordersArray, orderIdsToCancel])>"
    }
  ]
}

Where ordersArray is an array of OrderStore.Order structs, same layout as in the Submitting an Order section.

3. Positions & Open Interest

(PositionStore.sol + Positions.sol)

3.1 Position Structure

Contract: PositionStore

struct Position {
    address user;        // User that submitted the position
    address asset;       // Asset address, e.g. address(0) for native
    string  market;      // Market this position was submitted on
    bool    isLong;      // Whether the position is long or short
    uint256 size;        // Position size = margin * leverage (in wei)
    uint256 margin;      // Collateral tied to this position (in wei)
    int256  fundingTracker; // Market funding tracker at last update
    uint256 price;       // Average execution price of the position
    uint256 timestamp;   // Creation timestamp
}

You’ll mainly read this struct via PositionStore getters.


3.2 Open Interest (OI) Getters

PositionStore tracks open interest per (asset, market) and side.

Internal mappings:

  • OI[asset][market] (total)

  • OILong[asset][market]

  • OIShort[asset][market]

3.2.1 getOI

/// @notice Returns open interest of `asset` and `market`
function getOI(
    address asset,
    string calldata market
) external view returns (uint256) {
    return OILong[asset][market] + OIShort[asset][market];
}

Use this to get total OI (long + short).


3.2.2 getOILong

/// @notice Returns open interest of long positions
function getOILong(
    address asset,
    string calldata market
) external view returns (uint256) {
    return OILong[asset][market];
}

3.2.3 getOIShort

/// @notice Returns open interest of short positions
function getOIShort(
    address asset,
    string calldata market
) external view returns (uint256) {
    return OIShort[asset][market];
}

RPC example (OI)

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "id": 1,
  "params": [
    {
      "to": "<POSITIONSTORE_ADDRESS>",
      "data": "0x<encodeFunctionData('getOILong', [asset, 'ETH-USD'])>"
    },
    "latest"
  ]
}

Same pattern for getOI and getOIShort.


3.3 Single Position Lookup

3.3.1 getPosition(user, asset, market)

/// @notice Returns position of `user`
/// @param asset Base asset of position
/// @param market Market this position was submitted on
function getPosition(
    address user,
    address asset,
    string memory market
) public view returns (Position memory) {
    bytes32 key = _getPositionKey(user, asset, market);
    return positions[key];
}
  • If the user has no position for that (asset, market), all fields will be default (zeroed).

RPC example:

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "id": 1,
  "params": [
    {
      "to": "<POSITIONSTORE_ADDRESS>",
      "data": "0x<encodeFunctionData('getPosition', [user, asset, 'ETH-USD'])>"
    },
    "latest"
  ]
}

3.4 Batch Position Lookups

3.4.1 getPositions(users[], assets[], markets[])

Fetch multiple positions by triplets (user[i], asset[i], market[i]).

/// @notice Returns positions of `users`
/// @param assets Base assets of positions
/// @param markets Markets of positions
function getPositions(
    address[] calldata users,
    address[] calldata assets,
    string[] calldata markets
) external view returns (Position[] memory) {
    uint256 length = users.length;
    require(
        length == assets.length && length == markets.length,
        "Invalid array lengths"
    );

    Position[] memory _positions = new Position[](length);

    for (uint256 i = 0; i < length; i++) {
        _positions[i] = getPosition(users[i], assets[i], markets[i]);
    }

    return _positions;
}

Use this when building analytics/monitoring for multiple accounts or markets in a single call.


3.4.2 getPositions(keys[])

If you already know the position keys (bytes32), you can query by key:

/// @notice Returns positions
/// @param keys Position keys
function getPositions(
    bytes32[] calldata keys
) external view returns (Position[] memory) {
    uint256 length = keys.length;
    Position[] memory _positions = new Position[](length);

    for (uint256 i = 0; i < length; i++) {
        _positions[i] = positions[keys[i]];
    }

    return _positions;
}

The keys are just:

function _getPositionKey(
    address user,
    address asset,
    string memory market
) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked(user, asset, market));
}

You can reproduce this hash off-chain to build the keys[] array.


3.5 Global Position Enumeration

For infra/indexers / risk engines.

3.5.1 getPositionCount()

/// @notice Returns number of positions
function getPositionCount() external view returns (uint256) {
    return positionKeys.length();
}

3.5.2 getPositions(length, offset)

/// @notice Returns `length` amount of positions starting from `offset`
function getPositions(
    uint256 length,
    uint256 offset
) external view returns (Position[] memory) {
    uint256 _length = positionKeys.length();
    require(offset <= _length, "Offset out of bounds");

    uint256 availableLength = _length - offset;
    uint256 resultLength = length > availableLength
        ? availableLength
        : length;

    Position[] memory _positions = new Position[](resultLength);

    for (uint256 i = 0; i < resultLength; i++) {
        _positions[i] = positions[positionKeys.at(i + offset)];
    }

    return _positions;
}

Typical pagination pattern:

  1. Call getPositionCount()N

  2. Choose pageSize (e.g. 100)

  3. For each page:

    • offset = pageIndex * pageSize

    • getPositions(pageSize, offset)


3.6 Per-User Enumeration

3.6.1 getUserPositions(user)

/// @notice Returns all positions of `user`
function getUserPositions(
    address user
) external view returns (Position[] memory) {
    uint256 length = positionKeysForUser[user].length();
    Position[] memory _positions = new Position[](length);

    for (uint256 i = 0; i < length; i++) {
        _positions[i] = positions[positionKeysForUser[user].at(i)];
    }

    return _positions;
}

Use this for:

  • portfolio pages (“All open positions for this wallet”)

  • API responses listing user exposure

RPC example:

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "id": 1,
  "params": [
    {
      "to": "<POSITIONSTORE_ADDRESS>",
      "data": "0x<encodeFunctionData('getUserPositions', [user])>"
    },
    "latest"
  ]
}

3.7 PnL & Funding Fee Helper (Positions)

Contract: Positions

To compute PnL and funding fee for a position, you can call:

/// @param asset Base asset of position
/// @param market Market position was submitted on
/// @param isLong Whether position is long or short
/// @param price Current market price
/// @param positionPrice Average execution price of position
/// @param size Position size (margin * leverage)
/// @param fundingTracker Position funding tracker
/// @return pnl Profit and loss of position
/// @return fundingFee Funding fee of position
function getPnL(
    address asset,
    string memory market,
    bool isLong,
    uint256 price,
    uint256 positionPrice,
    uint256 size,
    int256 fundingTracker
) public view returns (int256 pnl, int256 fundingFee) {
    if (price == 0 || positionPrice == 0 || size == 0) return (0, 0);

    if (isLong) {
        pnl =
            (int256(size) * (int256(price) - int256(positionPrice))) /
            int256(positionPrice);
    } else {
        pnl =
            (int256(size) * (int256(positionPrice) - int256(price))) /
            int256(positionPrice);
    }

    int256 currentFundingTracker = funding.getRealTimeFundingTracker(
        asset,
        market
    );

    fundingFee =
        ((currentFundingTracker - fundingTracker) * int256(size)) /
        1e18;
}

So the integration flow to compute PnL for a user is:

  1. Fetch position from PositionStore.getPosition(user, asset, market)

  2. Fetch current price (via oracle / your own price feed)

  3. Call Positions.getPnL(asset, market, isLong, currentPrice, position.price, position.size, position.fundingTracker)

Or, if you don’t want to call getPnL on-chain, you can mirror that formula off-chain using:

  • Funding.getRealTimeFundingTracker(asset, market) (see more in section 6)

  • position.fundingTracker, position.size, position.price

4. Market Parameters

(MarketStore.sol)

4.1 Overview

MarketStore holds all configuration for every trading market:

  • leverage limits

  • funding parameters

  • execution settings

  • oracle settings

  • fees

  • liquidation thresholds

  • miscellaneous guards

This is the contract integrators query to:

  • list all available markets

  • fetch parameters for a specific market

  • validate user inputs client-side

  • build UI dashboards

  • build risk engines

  • compute pre-trade constraints


4.2 Market Structure

Every market is stored as a MarketStore.Market struct:

struct Market {
    string name;
    string category;
    address chainlinkFeed;
    uint256 maxLeverage;
    uint256 maxDeviation;
    uint256 fee;
    uint256 liqThreshold;
    uint256 fundingFactor;
    uint256 minOrderAge;
    uint256 pythMaxAge;
    bytes32 pythFeed;
    bool allowChainlinkExecution;
    bool isReduceOnly;
    uint256 minFactor;
    uint256 sampleSize;
}

Here is a human-readable table:

Field
Type
Description

name

string

Market symbol, e.g. "ETH-USD".

category

string

"crypto", "forex", "commodities", "indices", etc.

chainlinkFeed

address

Deprecated; not used in execution logic

maxLeverage

uint256

Max leverage allowed (e.g. 50e18 for 50×).

maxDeviation

uint256

Max price deviation allowed during execution (bps).

fee

uint256

Trading fee rate in basis points.

liqThreshold

uint256

Liquidation threshold in basis points.

fundingFactor

uint256

Annualized funding factor applied when OI is 100% skewed.

minOrderAge

uint256

Min seconds an order must wait before eligible for execution (anti-front-running guard).

pythMaxAge

uint256

Max staleness tolerated for Pyth prices.

pythFeed

bytes32

Pyth feed ID for this market.

allowChainlinkExecution

bool

Deprecated; not used in execution logic

isReduceOnly

bool

If true, all new orders for this market must be reduce-only.

minFactor

uint256

Minimum funding factor (per interval) after capping.

sampleSize

uint256

Number of Pyth samples used for EMA funding calculations.


4.3 Reading Market Information via RPC

All functions are view → use eth_call.


4.3.1 get(market)

Signature:

function get(string memory market)
    external
    view
    returns (Market memory);

RPC example:

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "id": 1,
  "params": [
    {
      "to": "<MARKETSTORE_ADDRESS>",
      "data": "0x<encodeFunctionData('get', ['ETH-USD'])>"
    },
    "latest"
  ]
}

Returns the full Market struct.


4.3.2 getMany(markets[])

Fetch multiple market configs at once.

function getMany(string[] memory markets)
    external
    view
    returns (Market[] memory);

Useful for dashboards or front-end preloading.


4.3.3 Get the list of all markets

Get the full list:

function getMarketList()
    external
    view
    returns (string[] memory);

Get number of markets:

function getMarketCount()
    external
    view
    returns (uint256);

Indexed access (pagination-friendly):

function getMarketByIndex(uint256 index)
    external
    view
    returns (string memory);

Pattern for full enumeration:

  1. count = getMarketCount()

  2. Loop from 0 → count-1 calling getMarketByIndex(i)


4.4 Why integrators usually read MarketStore

1. Validate client-side order submissions

  • Ensure selected leverage ≤ maxLeverage

  • Ensure market is not isReduceOnly

  • Ensure Pyth prices are not stale (pythMaxAge)

2. Risk dashboards

  • Display max leverage, funding factor, fee structure.

  • Warn user if a market is currently reduce-only.

  • Show oracle configuration.

3. Funding engines

  • Funding factor + EMA samples.

  • Combine with FundingStore (next section).


4.5 RPC Example — Full Market Fetch

Using ethers.js (for integrators):

const iface = new ethers.utils.Interface(MarketStoreAbi);

const data = iface.encodeFunctionData("get", ["ETH-USD"]);

const result = await provider.call({
  to: MARKETSTORE_ADDRESS,
  data
});

// decode
const market = iface.decodeFunctionResult("get", result);

console.log(market);

Output will be the full Market struct with all fields in order.

5. Risk Parameters

(RiskStore.sol)

5.1 Overview

RiskStore centralizes protocol-level risk limits:

  • Per-market caps

    • maxDelta (long-short imbalance cap)

    • maxOI (total open interest cap)

    • maxPositionSizeFactor (max position size as a fraction of maxOI)

  • Check helpers

    • checkMaxDelta, checkMaxOI, checkMaxPositionSize

Most of these are governance-set and read-only for integrators, except the check* functions which you can use via eth_call to simulate whether an action would pass risk checks.


5.2 Stored Risk State

5.2.1 Per-market caps

// V2 Delta Long-Short
mapping(string => mapping(address => uint256)) private maxDelta;
// market => asset => amount

// V3 Max Position Size
uint256 private maxPositionSizeFactor; // 10000 = 100%
  • maxDelta[market][asset]

    Maximum long-short imbalance allowed for (market, asset).

  • maxPositionSizeFactor

    Global factor used to derive max position size per user for each (market, asset):

    maxPositionSize = maxOI[market][asset] * maxPositionSizeFactor / 10000;
    

5.2.2 Open interest cap (per market)

mapping(string => mapping(address => uint256)) private maxOI;
// market => asset => amount
  • maxOI[market][asset]

    Maximum total open interest allowed for that (market, asset).


5.3 Read-Only Getters (RPC)

All of these are view → use eth_call.

5.3.1 Max Delta

/// @notice Get maximum delta of `market`
/// @param market Market to check
/// @param asset Address of base asset, e.g. address(0) for ETH
function getMaxDelta(
    string calldata market,
    address asset
) external view returns (uint256) {
    return maxDelta[market][asset];
}

5.3.2 Max OI

/// @notice Get maximum open interest of `market`
function getMaxOI(
    string calldata market,
    address asset
) external view returns (uint256) {
    return maxOI[market][asset];
}

5.3.3 Max Position Size

/// @notice Get max position size for a given market and asset
/// @dev Computed as maxOI * maxPositionSizeFactor / 10000
function getMaxPositionSize(
    string calldata market,
    address asset
) external view returns (uint256) {
    return (maxOI[market][asset] * maxPositionSizeFactor) / 10000;
}

This is the per-user cap the protocol enforces once maxPositionSizeFactor is set.


5.4 Risk Check Functions (Pre-trade Simulation)

These revert on failure and otherwise return nothing.

You can call them via eth_call off-chain to simulate whether a given trade will pass risk rules.

5.4.1 checkMaxOI

function checkMaxOI(
    address asset,
    string calldata market,
    uint256 size
) external view {
    uint256 openInterest = PositionStore(DS.getAddress("PositionStore"))
        .getOI(asset, market);
    uint256 _maxOI = maxOI[market][asset];
    if (_maxOI > 0 && openInterest + size > _maxOI) revert("!max-oi");
}
  • size = additional size for the new/expanded position.

  • If maxOI == 0 → no limit enforced.

  • If it reverts with "!max-oi", the trade would exceed total OI.


5.4.2 checkMaxDelta

function checkMaxDelta(
    address asset,
    string calldata market,
    uint256 size,
    bool isLong
) external view {
    uint256 oiLong = PositionStore(DS.getAddress("PositionStore"))
        .getOILong(asset, market);
    uint256 oiShort = PositionStore(DS.getAddress("PositionStore"))
        .getOIShort(asset, market);
    uint256 _maxDelta = maxDelta[market][asset];

    if (_maxDelta > 0) {
        if (isLong) {
            // available long = maxDelta - oiLong + oiShort
            int256 availableLong =
                int256(_maxDelta) - int256(oiLong) + int256(oiShort);
            if (availableLong < 0) {
                availableLong = 0;
            }
            require(size <= uint256(availableLong), "!max-delta");
        } else {
            // available short = maxDelta + oiLong - oiShort
            int256 availableShort =
                int256(_maxDelta) + int256(oiLong) - int256(oiShort);
            if (availableShort < 0) {
                availableShort = 0;
            }
            require(size <= uint256(availableShort), "!max-delta");
        }
    }
}

Interpretation:

  • maxDelta caps the net directional exposure:

    • for new longs, checks if there’s enough availableLong

    • for new shorts, checks availableShort

  • Reverts with "!max-delta" if the new size would push the market beyond allowed long-short imbalance.


5.4.3 checkMaxPositionSize

function checkMaxPositionSize(
    address asset,
    string calldata market,
    uint256 sizeToAdd,
    uint256 currentSize,
    bool isLongOrder,
    bool isLongPosition
) external view {
    if (maxPositionSizeFactor == 0) return;
    uint256 maxPositionSize =
        (maxOI[market][asset] * maxPositionSizeFactor) / 10000;

    uint256 newSize;
    if (currentSize == 0) {
        newSize = sizeToAdd;
    } else if (isLongOrder == isLongPosition) {
        newSize = currentSize + sizeToAdd;
    } else {
        newSize = currentSize > sizeToAdd
            ? currentSize - sizeToAdd
            : sizeToAdd - currentSize;
    }

    require(newSize <= maxPositionSize, "!max-position-size");
}

Interpretation:

  • If maxPositionSizeFactor == 0 → feature disabled, no check.

  • Otherwise:

    • maxPositionSize is derived from maxOI.

    • newSize is the resulting size after the order:

      • same direction → current + sizeToAdd

      • opposite direction → netting long/short

  • Reverts with "!max-position-size" if user would exceed the allowed per-position cap.

You can use this from off-chain to simulate whether a user’s order will be accepted.


5.5 Example: Pre-trade Risk Simulation (off-chain)

To simulate whether a new order would pass risk checks (without sending a tx):

  1. Compute sizeToAdd based on order margin & leverage.

  2. Fetch:

    • Current position size (PositionStore.getPosition)

    • Current OI (PositionStore.getOI, getOILong, getOIShort)

  3. Call via eth_call (same calldata as on-chain):

    • RiskStore.checkMaxOI(asset, market, sizeToAdd)

    • RiskStore.checkMaxDelta(asset, market, sizeToAdd, isLong)

    • RiskStore.checkMaxPositionSize(asset, market, sizeToAdd, currentSize, isLongOrder, isLongPosition)

If all three calls do not revert, the order is within risk limits.

6. Funding rates

(Funding.sol + FundingStore.sol)

6.1 Overview

Funding is computed per interval, 8 hours based on:

  • long vs short open interest (OI)

  • market funding config (fundingFactor, minFactor, sampleSize)

  • risk config (maxDelta from RiskStore)

State is stored in FundingStore:

  • fundingInterval — length of one funding period (seconds)

  • fundingTrackers[asset][market] — cumulative funding index

  • lastUpdated[asset][market] — last time funding was updated

  • lastEmaFundingRate[asset][market] — uncapped EMA funding rate

  • lastCappedEmaFundingRate[asset][market] — EMA funding rate after minFactor clamp

The Funding contract exposes read helpers to:

  • compute accrued funding since lastUpdated

  • compute EMA / capped funding rates

  • compute real-time funding tracker between discrete updates

All of this is view → you can hit it with eth_call.


6.2 FundingStore: Stored State

Contract: FundingStore

6.2.1 Variables

uint256 public fundingInterval; // default: 8 hours

// asset => market => funding tracker (long side; short is opposite)
mapping(address => mapping(string => int256)) private fundingTrackers;

// asset => market => last update timestamp (seconds)
mapping(address => mapping(string => uint256)) private lastUpdated;

// asset => market => last EMA funding rate (bps × 1e18)
mapping(address => mapping(string => int256)) private lastEmaFundingRate;

// asset => market => capped EMA funding rate (bps × 1e18)
mapping(address => mapping(string => int256))
    private lastCappedEmaFundingRate;

6.2.2 Getters (RPC)

All are vieweth_call.

fundingInterval()

function fundingInterval() external view returns (uint256);

Returns the current funding interval (in seconds).


getLastUpdated(asset, market)

function getLastUpdated(
    address asset,
    string calldata market
) external view returns (uint256);

Returns the last timestamp funding was updated for (asset, market).


getFundingTracker(asset, market)

function getFundingTracker(
    address asset,
    string calldata market
) external view returns (int256);

Returns the cumulative funding tracker.

This is what’s used in Positions.getPnL:

fundingFee =
    ((currentFundingTracker - position.fundingTracker) * int256(size)) /
    1e18;

getFundingTrackers(assets[], markets[])

function getFundingTrackers(
    address[] calldata assets,
    string[] calldata markets
) external view returns (int256[] memory fts);

Batch version: returns an array of fundingTrackers[asset[i]][market[i]].


getLastEmaFundingRate(asset, market)

function getLastEmaFundingRate(
    address asset,
    string calldata market
) external view returns (int256);
  • Stored EMA funding rate before applying minFactor clamping.

  • Unit: bps × 1e18 (scaled by UNIT = 1e18).


getLastCappedEmaFundingRate(asset, market)

function getLastCappedEmaFundingRate(
    address asset,
    string calldata market
) external view returns (int256);
  • EMA funding rate after clamping with minFactor (from MarketStore.Market).

  • Also in bps × 1e18.

This is what getRealTimeFundingTracker uses to interpolate funding between updates.


6.3 Funding: Core Read Helpers

Contract: Funding

Key constants/refs:

uint256 public constant UNIT = 10 ** 18;

DataStore public DS;
FundingStore public fundingStore;
MarketStore public marketStore;
PositionStore public positionStore;
RiskStore public riskStore;

6.3.1 getAccruedFundingV2(asset, market, intervals)

function getAccruedFundingV2(
    address asset,
    string memory market,
    uint256 intervals
) public view returns (
    int256 fundingIncrement,
    int256 emaFundingRate,
    int256 onePeriodFundingIncrement,
    int256 cappedEmaFundingRate
);

This is the actual funding engine, using:

  • EMA smoothing

  • maxDelta from RiskStore

  • minFactor and sampleSize from MarketStore

Behavior:

  • If intervals == 0: same logic as above; compute intervals from time delta.

  • If intervals == 0 afterward → returns (0, 0, 0, 0).

  • Fetch OI:

    uint256 OILong = positionStore.getOILong(asset, market);
    uint256 OIShort = positionStore.getOIShort(asset, market);
    if (OIShort == 0 && OILong == 0) return (0, 0, 0, 0);
    
  • Fetch configs:

    MarketStore.Market memory marketInfo = marketStore.get(market);
    
    uint256 yearlyFundingFactor = marketInfo.fundingFactor;
    uint256 maxDelta = riskStore.getMaxDelta(market, asset);
    int256 lastEmaFundingRate = fundingStore.getLastEmaFundingRate(asset, market);
    uint256 minFactor = marketInfo.minFactor;
    uint256 sampleSize = marketInfo.sampleSize;
    
  • Compute absolute delta:

    uint256 absDelta = OIShort > OILong
        ? OIShort - OILong
        : OILong - OIShort;
    
  • Early exit if:

    if (minFactor == 0 || sampleSize == 0 || absDelta == 0 || maxDelta == 0) {
        return (0, 0, 0, 0);
    }
    
  • Compute directional ratio:

    int256 deltaRatio = int256((absDelta * UNIT) / maxDelta);
    if (OILong < OIShort) deltaRatio = -deltaRatio;
    // then clamp into [-UNIT, UNIT]
    
  • Funding rate FR(Δ):

    // FR(Δ) = FundingFactor * clamp(Δ/MaxDelta, -1, 1)
    int256 FR = int256(yearlyFundingFactor) * deltaRatio;
    
  • EMA:

    // α = 2 * UNIT / (N + 1)
    int256 alpha = (2 * int256(UNIT)) / (int256(sampleSize) + 1);
    
    // FR_ema(t) = α*FR(t) + (1-α)*FR_ema(t-1)
    int256 emaFundingRate = (alpha * FR
        + (int256(UNIT) - alpha) * lastEmaFundingRate
    ) / int256(UNIT);
    
  • Apply minFactor clamp:

    int256 incr = emaFundingRate;
    
    if (incr > 0 && incr < int256(minFactor) * int256(UNIT)) {
        incr = int256(minFactor) * int256(UNIT);
    } else if (incr < 0 && incr > -int256(minFactor) * int256(UNIT)) {
        incr = -int256(minFactor) * int256(UNIT);
    }
    
  • Turn this rate into accumulated funding:

    uint256 accruedFunding = (uint256(incr > 0 ? incr : -incr)
        * intervals) / (24 * 365);
    
    uint256 onePeriodFundingIncrement = (uint256(incr > 0 ? incr : -incr))
        / (24 * 365);
    
  • Sign again based on which side pays:

    if (OILong > OIShort) {
        // longs pay shorts → tracker increases
        return (
            int256(accruedFunding),
            emaFundingRate,
            int256(onePeriodFundingIncrement),
            incr
        );
    } else {
        // shorts pay longs → tracker decreases
        return (
            -1 * int256(accruedFunding),
            emaFundingRate,
            -1 * int256(onePeriodFundingIncrement),
            incr
        );
    }
    

Returned values (for docs / UIs):

  1. fundingIncrement

    • Signed funding tracker increment over the given intervals.

  2. emaFundingRate

    • Updated uncapped EMA funding rate (bps × 1e18).

  3. onePeriodFundingIncrement

    • Signed funding tracker increment for one interval.

  4. cappedEmaFundingRate (incr)

    • EMA funding rate after minFactor clamp (bps × 1e18).


6.3.2 getRealTimeFundingTracker(asset, market)

function getRealTimeFundingTracker(
    address asset,
    string calldata market
) public view returns (int256);

This returns a smooth, interpolated funding tracker between discrete updates.

Implementation:

int256 currentFundingTracker = fundingStore.getFundingTracker(asset, market);
uint256 lastUpdated = fundingStore.getLastUpdated(asset, market);

int256 totalPeriodFundingIncrement =
    fundingStore.getLastCappedEmaFundingRate(asset, market)
    / int256(24 * 365);

uint256 fundingInterval = fundingStore.fundingInterval();
uint256 ratio = (UNIT * (block.timestamp - lastUpdated)) / fundingInterval;
if (ratio == 0) return currentFundingTracker;

int256 realTimeFundingTracker = currentFundingTracker +
    (totalPeriodFundingIncrement * int256(ratio)) / int256(UNIT);

return realTimeFundingTracker;

Interpretation:

  • currentFundingTracker is the last “discrete” tracker value.

  • totalPeriodFundingIncrement is the per-hour funding increment derived from the annualized capped EMA rate (divided by24*365), then scaled by the fraction of the funding interval that has elapsed (ratio).

  • ratio is the fraction of the funding interval that has elapsed since lastUpdated (scaled by UNIT).

  • realTimeFundingTracker linearly interpolates between last update and the next.

This is the function you should use for funding calculations (and it’s exactly what Positions.getPnL uses indirectly via Funding.getRealTimeFundingTracker).


6.3.3 getRealTimeFundingTrackers(asset, markets[])

function getRealTimeFundingTrackers(
    address asset,
    string[] calldata markets
) external view returns (int256[] memory);

Batch version: for a given collateral asset, returns an array of getRealTimeFundingTracker(asset, markets[i]).

  • horizontal table: market / funding (annualized) / funding (8h)

  • or for your internal monitoring.


6.4 Typical Integration Patterns

6.4.1 Show live funding per market

For each (asset, market):

  1. Read realTimeFundingTracker = Funding.getRealTimeFundingTracker(asset, market)

  2. Read position.fundingTracker for a hypothetical unit size

  3. Approximate per-interval funding rate or convert to annualized % as you like (you already use this logic in getPnL).

Or simply:

  • get getLastCappedEmaFundingRate(asset, market)

  • convert from (bps × 1e18) to “annualized %”.


6.4.2 Show “funding snapshot” per market

For each (asset, market):

  • From FundingStore:

    • fundingInterval()

    • getLastUpdated(asset, market)

    • getFundingTracker(asset, market)

    • getLastEmaFundingRate(asset, market)

    • getLastCappedEmaFundingRate(asset, market)

  • From Funding:

    • getRealTimeFundingTracker(asset, market)

    • getAccruedFundingV2(asset, market, 1) to show “next interval increment”.


6.4.3 Use funding inside PnL (already wired)

As seen before:

  • Positions.getPnL calls Funding.getRealTimeFundingTracker internally.

  • You can replicate the same calculation off-chain using:

    • Funding.getRealTimeFundingTracker

    • position.fundingTracker

    • position.size

Last updated