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:
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
DataStoreinstead 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 hoursfundingStore.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:
Read market configuration (
MarketStore.get)Fetch user positions (
PositionStore.getPosition/getUserPositions)Check risk limits (
RiskStore.checkMaxOI,checkMaxDelta, etc. viaeth_call)Submit order (
Orders.submitOrder)Track execution via
eth_getLogs(OrderCreated,OrderExecuted)Compute PnL using
Positions.getPnLor replicating the logic off-chainMonitor 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:
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
submitOrder FunctionContract: Orders
function submitOrder(
OrderStore.Order memory params,
uint256 tpPrice,
uint256 slPrice
) external payable;
Key Rules
If
tpPrice > 0orslPrice > 0:→ The main order will always be
isReduceOnly = falseTP/SL orders created automatically will be
isReduceOnly = true.Limit/stop rules:
orderType = 1 or 2requiresprice > 0.For short orders (
isLong = false), using the current market priceP: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 TTLOtherwise must respect per-market max TTL.
Collateral & fees:
If
asset == address(0)(native):msg.valuemust include margin + fee
If ERC20:
User must approve
FundStorefor at least margin + feemsg.valueis ignored (and refunded if present)
1.3 TP/SL Behavior
tpPrice and slPrice are optional trigger prices.
Validation logic:
For long:
tpPrice > entryPriceslPrice < entryPrice
For short:
tpPrice < entryPriceslPrice > entryPrice
If both TP and SL are provided:
Long:
tpPrice > slPriceShort:
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
Option 1 — Listen to Events (recommended)
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
useras 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:
Send the tx
After confirmation, call
oid()Query
get(orderId)
2. Cancel & Batch Orders
(Orders.sol + OrderStore.sol)
2.1 Overview
There are three main flows:
User-initiated cancellations:
cancelOrder(uint256 orderId)cancelOrders(uint256[] orderIds)
Protocol/infra-initiated cancellations (keepers, processor, etc.):
cancelOrder(uint256 orderId, string reason)cancelOrders(uint256[] orderIds, string[] reasons)
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
cancelOrderContract: 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
cancelOrdersContract: Orders
function cancelOrders(uint256[] calldata orderIds) external ifNotPaused;
Behavior:
Iterates through
orderIds.For each:
Fetches the order from
OrderStore.If
order.size > 0andorder.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)
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)
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 withreasons[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 + feefromFundStore.
An
OrderCancelledevent is emitted with:orderIduserreason
2.5 Batch Submit + Optional Cancel: submitSimpleOrders
submitSimpleOrdersThis 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
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.
Submit phase
uint256 totalValueConsumed; for (uint256 i = 0; i < params.length; i++) { uint256 valueConsumed; (, valueConsumed) = _submitOrder(params[i]); totalValueConsumed += valueConsumed; }Calls
_submitOrderfor eachparams[i].valueConsumedis:margin + feeif collateral is native (e.g. ETH),0if collateral is ERC20 (because tokens are pulled viatransferIn).
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
Position StructureContract: 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
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
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
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)
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[])
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[])
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()
getPositionCount()/// @notice Returns number of positions
function getPositionCount() external view returns (uint256) {
return positionKeys.length();
}
3.5.2 getPositions(length, offset)
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:
Call
getPositionCount()→NChoose
pageSize(e.g. 100)For each page:
offset = pageIndex * pageSizegetPositions(pageSize, offset)
3.6 Per-User Enumeration
3.6.1 getUserPositions(user)
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)
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:
Fetch position from
PositionStore.getPosition(user, asset, market)Fetch current price (via oracle / your own price feed)
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:
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)
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[])
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:
count = getMarketCount()Loop from
0 → count-1callinggetMarketByIndex(i)
4.4 Why integrators usually read MarketStore
1. Validate client-side order submissions
Ensure selected leverage ≤
maxLeverageEnsure market is not
isReduceOnlyEnsure 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).maxPositionSizeFactorGlobal 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
checkMaxOIfunction 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
checkMaxDeltafunction 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:
maxDeltacaps the net directional exposure:for new longs, checks if there’s enough
availableLongfor 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
checkMaxPositionSizefunction 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:
maxPositionSizeis derived frommaxOI.newSizeis the resulting size after the order:same direction →
current + sizeToAddopposite 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):
Compute
sizeToAddbased on order margin & leverage.Fetch:
Current position size (
PositionStore.getPosition)Current OI (
PositionStore.getOI,getOILong,getOIShort)
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 (
maxDeltafromRiskStore)
State is stored in FundingStore:
fundingInterval— length of one funding period (seconds)fundingTrackers[asset][market]— cumulative funding indexlastUpdated[asset][market]— last time funding was updatedlastEmaFundingRate[asset][market]— uncapped EMA funding ratelastCappedEmaFundingRate[asset][market]— EMA funding rate after minFactor clamp
The Funding contract exposes read helpers to:
compute accrued funding since
lastUpdatedcompute 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 view → eth_call.
fundingInterval()
fundingInterval()function fundingInterval() external view returns (uint256);
Returns the current funding interval (in seconds).
getLastUpdated(asset, market)
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)
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[])
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)
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)
getLastCappedEmaFundingRate(asset, market)function getLastCappedEmaFundingRate(
address asset,
string calldata market
) external view returns (int256);
EMA funding rate after clamping with
minFactor(fromMarketStore.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)
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
maxDeltafromRiskStoreminFactorandsampleSizefromMarketStore
Behavior:
If
intervals == 0: same logic as above; compute intervals from time delta.If
intervals == 0afterward → 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
minFactorclamp: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):
fundingIncrementSigned funding tracker increment over the given
intervals.
emaFundingRateUpdated uncapped EMA funding rate (bps × 1e18).
onePeriodFundingIncrementSigned funding tracker increment for one interval.
cappedEmaFundingRate(incr)EMA funding rate after minFactor clamp (bps × 1e18).
6.3.2 getRealTimeFundingTracker(asset, market)
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:
currentFundingTrackeris the last “discrete” tracker value.totalPeriodFundingIncrementis 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).ratiois the fraction of the funding interval that has elapsed sincelastUpdated(scaled byUNIT).realTimeFundingTrackerlinearly 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[])
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):
Read
realTimeFundingTracker = Funding.getRealTimeFundingTracker(asset, market)Read
position.fundingTrackerfor a hypothetical unit sizeApproximate 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.getPnLcallsFunding.getRealTimeFundingTrackerinternally.You can replicate the same calculation off-chain using:
Funding.getRealTimeFundingTrackerposition.fundingTrackerposition.size
Last updated