# Smart contract documentation

*PinguSDK is now live:* \
*-* [*https://insights.pingu.exchange/posts/pingu-sdk-is-live*](https://insights.pingu.exchange/posts/pingu-sdk-is-live)\
*-* [*https://github.com/PinguProtocol/pingu-sdk*](https://github.com/PinguProtocol/pingu-sdk)

## **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.

***

### **0.5 Recommended Workflow for Integrators**

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`

```solidity
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**

```json
{
  "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)**

```jsx
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**

#### **Option 1 — Listen to Events (recommended)**

Event emitted:

```solidity
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:

```solidity
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:

```solidity
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`

```solidity
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):**

```json
{
  "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`

```solidity
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:

    ```solidity
    _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):**

```json
{
  "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)

```solidity
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)

```solidity
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:

```solidity
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`

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

```

#### Behavior

1. **Cancel phase**

   ```solidity
   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**

   ```solidity
   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**

   ```solidity
   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:

```json
{
  "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`

```solidity
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`

```solidity
/// @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`

```solidity
/// @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`

```solidity
/// @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)

```json
{
  "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)`

```solidity
/// @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:**

```json
{
  "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])`.

```solidity
/// @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:

```solidity
/// @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:

```solidity
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()`

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

```

***

#### 3.5.2 `getPositions(length, offset)`

```solidity
/// @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)`

```solidity
/// @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:**

```json
{
  "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:

```solidity
/// @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:

```solidity
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:**

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

```

**RPC example:**

```json
{
  "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.

```solidity
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:**

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

#### **Get number of markets:**

```solidity
function getMarketCount()
    external
    view
    returns (uint256);
```

#### **Indexed access (pagination-friendly):**

```solidity
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):

```jsx
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

```solidity
// 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)`:

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

  ```

#### 5.2.2 Open interest cap (per market)

```solidity
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

```solidity
/// @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

```solidity
/// @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

```solidity
/// @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`

```solidity
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`

```solidity
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`

```solidity
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

```solidity
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 `view` → `eth_call`.

#### `fundingInterval()`

```solidity
function fundingInterval() external view returns (uint256);

```

Returns the current funding interval (in seconds).

***

#### `getLastUpdated(asset, market)`

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

```

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

***

#### `getFundingTracker(asset, market)`

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

```

Returns the **cumulative funding tracker**.

This is what’s used in `Positions.getPnL`:

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

```

***

#### `getFundingTrackers(assets[], markets[])`

```solidity
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)`

```solidity
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)`

```solidity
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:

```solidity
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)`

```solidity
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:

  ```solidity
  uint256 OILong = positionStore.getOILong(asset, market);
  uint256 OIShort = positionStore.getOIShort(asset, market);
  if (OIShort == 0 && OILong == 0) return (0, 0, 0, 0);

  ```
* Fetch configs:

  ```solidity
  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:

  ```solidity
  uint256 absDelta = OIShort > OILong
      ? OIShort - OILong
      : OILong - OIShort;

  ```
* Early exit if:

  ```solidity
  if (minFactor == 0 || sampleSize == 0 || absDelta == 0 || maxDelta == 0) {
      return (0, 0, 0, 0);
  }

  ```
* Compute directional ratio:

  ```solidity
  int256 deltaRatio = int256((absDelta * UNIT) / maxDelta);
  if (OILong < OIShort) deltaRatio = -deltaRatio;
  // then clamp into [-UNIT, UNIT]

  ```
* Funding rate FR(Δ):

  ```solidity
  // FR(Δ) = FundingFactor * clamp(Δ/MaxDelta, -1, 1)
  int256 FR = int256(yearlyFundingFactor) * deltaRatio;

  ```
* EMA:

  ```solidity
  // α = 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:

  ```solidity
  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**:

  ```solidity
  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:

  ```solidity
  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)`

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

```

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

Implementation:

```solidity
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 by`24*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[])`

```solidity
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`
