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
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
Encoding Example (ethers.js)
1.5 Getting the Submitted Order ID
Option 1 — Listen to Events (recommended)
Event emitted:
You can filter logs via eth_getLogs using:
event signature hash
useras indexed topic
Option 2 — Query the store
OrderStore exposes:
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:
which refunds margin+fee (for non-reduce-only orders) and emits OrderCancelled.
2.2 User-Initiated Cancellation
2.2.1 cancelOrder
cancelOrderContract: Orders
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):
2.2.2 cancelOrders
cancelOrdersContract: Orders
Behavior:
Iterates through
orderIds.For each:
Fetches the order from
OrderStore.If
order.size > 0andorder.user == msg.sender, calls:Otherwise it silently skips the ID.
This is the preferred way to cancel multiple user orders in a single tx.
RPC example (batch):
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)Typical reasons: "insufficient-margin", "stale-price", "risk-limit", etc.
2.3.2 cancelOrders (batch with reasons)
cancelOrders (batch with reasons)Both arrays must have the same length.
Each
orderIds[i]is cancelled withreasons[i].
2.4 Internal Cancellation Logic
All paths end up in:
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
Behavior
Cancel phase
Same checks as
cancelOrders:only cancels if the order exists and belongs to
msg.sender.
Submit phase
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 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:
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
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
getOIUse this to get total OI (long + short).
3.2.2 getOILong
getOILong3.2.3 getOIShort
getOIShortRPC example (OI)
Same pattern for getOI and getOIShort.
3.3 Single Position Lookup
3.3.1 getPosition(user, asset, market)
getPosition(user, asset, market)If the user has no position for that
(asset, market), all fields will be default (zeroed).
RPC example:
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]).
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:
The keys are just:
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()3.5.2 getPositions(length, offset)
getPositions(length, offset)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)Use this for:
portfolio pages (“All open positions for this wallet”)
API responses listing user exposure
RPC example:
3.7 PnL & Funding Fee Helper (Positions)
Positions)Contract: Positions
To compute PnL and funding fee for a position, you can call:
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:
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:
RPC example:
Returns the full Market struct.
4.3.2 getMany(markets[])
getMany(markets[])Fetch multiple market configs at once.
Useful for dashboards or front-end preloading.
4.3.3 Get the list of all markets
Get the full list:
Get number of markets:
Indexed access (pagination-friendly):
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):
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
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):
5.2.2 Open interest cap (per market)
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
5.3.2 Max OI
5.3.3 Max Position Size
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
checkMaxOIsize= 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
checkMaxDeltaInterpretation:
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
checkMaxPositionSizeInterpretation:
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
6.2.2 Getters (RPC)
All are view → eth_call.
fundingInterval()
fundingInterval()Returns the current funding interval (in seconds).
getLastUpdated(asset, market)
getLastUpdated(asset, market)Returns the last timestamp funding was updated for (asset, market).
getFundingTracker(asset, market)
getFundingTracker(asset, market)Returns the cumulative funding tracker.
This is what’s used in Positions.getPnL:
getFundingTrackers(assets[], markets[])
getFundingTrackers(assets[], markets[])Batch version: returns an array of fundingTrackers[asset[i]][market[i]].
getLastEmaFundingRate(asset, market)
getLastEmaFundingRate(asset, market)Stored EMA funding rate before applying minFactor clamping.
Unit: bps × 1e18 (scaled by
UNIT = 1e18).
getLastCappedEmaFundingRate(asset, market)
getLastCappedEmaFundingRate(asset, market)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:
6.3.1 getAccruedFundingV2(asset, market, intervals)
getAccruedFundingV2(asset, market, intervals)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:
Fetch configs:
Compute absolute delta:
Early exit if:
Compute directional ratio:
Funding rate FR(Δ):
EMA:
Apply
minFactorclamp:Turn this rate into accumulated funding:
Sign again based on which side pays:
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)This returns a smooth, interpolated funding tracker between discrete updates.
Implementation:
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[])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