From cb14ae6db7ca604d7385b7cebeb7108c64b04069 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 11:46:37 +0000 Subject: [PATCH] docs: fix values, names, and shapes that don't match the implementation Corrects documentation that cites specific constants, addresses, method signatures, parameter shapes, response fields, units, thresholds, or defaults that differ from the merged podnetwork/pod implementation. Every corrected value was re-verified against pod @ origin/main (d6f4b46f) before editing. Highlights: - Testnet chain ID: 129301 -> 1293 (0x50d) - Recovery precompile address: 0x...04EC0EE4 -> 0x50d...0003 - pod_getVoteBatches params: (ranges, binary), not two nullable ints - pod_getBridgeClaimProof response: {proof, committee_epoch, aux_tx_suffix} with byte arrays, not {signatures, ..} hex strings - pod_getRecoveryTargetTx response field: hash, not txHash - ob_getCandles: (orderbook_id, query object), not 4 positional params - ob_getOrders: orderbook_id filter + paginated envelope w/ snake_case - ob_getOrderbook: snapshot capped at 10 levels per side - eth_blockNumber returns wall-clock seconds, not PPT microseconds - Native USD deposits must carry tx.value; bridge deposits are whitelisted-ERC-20 only (no 0xEeee... sentinel path) - Backstop threshold: (2/3) x maintenance margin, not 0.75 x IM - Batch interval: single global 500ms setting, not per-market 100-200ms - Finality: n - f count-based quorum, no stake weighting - pod_subscribe -> pod_waitPastPerfectTime (returns empty result) - max_funding_rate default: 4% per 8h window; mark_price_clamp naming - OrderStatus/CandleResolution enums and several always-present response fields (quote_volume, entry_funding, net_deposits, ...) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01A74BZVmHFAksnZWVaGBcgi --- doc/api-reference/README.md | 2 +- .../applications-precompiles/README.md | 22 ++-- .../applications-precompiles/orderbook.md | 6 +- doc/api-reference/guides/bridge-from-pod.md | 14 ++- doc/api-reference/guides/bridge-to-pod.md | 10 +- .../guides/place-a-perpetual-order.md | 2 +- doc/api-reference/guides/read-market-data.md | 17 ++- .../guides/recover-locked-account.md | 6 +- doc/api-reference/json-rpc/README.md | 4 +- doc/api-reference/json-rpc/openapi.yaml | 108 +++++++++++++----- doc/api-reference/market-configurations.md | 6 +- doc/protocol/margin.md | 16 +-- doc/protocol/markets-overview.md | 4 +- doc/protocol/network-architecture/README.md | 4 +- .../censorship-resistance.md | 2 +- .../network-architecture/local-ordering.md | 4 +- .../network-architecture/timestamping.md | 2 +- .../network-architecture/transaction-flow.md | 2 +- doc/protocol/orderbook.md | 8 +- doc/protocol/perpetuals.md | 8 +- 20 files changed, 157 insertions(+), 90 deletions(-) diff --git a/doc/api-reference/README.md b/doc/api-reference/README.md index c4b5f62f..0f0d2a17 100644 --- a/doc/api-reference/README.md +++ b/doc/api-reference/README.md @@ -26,7 +26,7 @@ View transactions and accounts | --------------- | ------------------------------------------------------------ | | Name | `pod` | | RPC | `https://rpc.podtestnet.dev` | -| Chain ID | `129301` | +| Chain ID | `1293` (`0x50d`) | | Explorer | `https://explorer.pod.network` | | Currency Symbol | `USD` | | EVM Version | `Prague` (Ethereum block 22,431,084, Released May 7th, 2025) | diff --git a/doc/api-reference/applications-precompiles/README.md b/doc/api-reference/applications-precompiles/README.md index 984c4676..078e5c17 100644 --- a/doc/api-reference/applications-precompiles/README.md +++ b/doc/api-reference/applications-precompiles/README.md @@ -16,7 +16,7 @@ You interact with Pod's precompiles the same way you would interact with any sma ### Reading State -Query the deposited balance of a token in the orderbook contract using `eth_call`. +Query an account's deposited balance of a token in the orderbook contract using `eth_call`. {% tabs %} {% tab title="JavaScript (ethers.js)" %} @@ -26,12 +26,11 @@ import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://rpc.podtestnet.dev"); const ORDERBOOK = "0x50d0000000000000000000000000000000000002"; -const abi = ["function balanceOf(address token, address account) view returns (uint256)"]; +const abi = ["function balanceOf(address token, address account) view returns (int256)"]; const orderbook = new ethers.Contract(ORDERBOOK, abi, provider); -const USDT = "0x0000000000000000000000000000000000000001"; -const ACCOUNT = "0xYourAddress"; -const balance = await orderbook.balanceOf(USDT, ACCOUNT); +const NVDAX = "0x0000000000000000000000000000000000000001"; // NVDAx (Tokenized NVIDIA) +const balance = await orderbook.balanceOf(NVDAX, "0xYourAddress"); console.log("Balance:", balance.toString()); ``` {% endtab %} @@ -46,14 +45,13 @@ ORDERBOOK = "0x50d0000000000000000000000000000000000002" abi = [{"inputs": [{"name": "token", "type": "address"}, {"name": "account", "type": "address"}], "name": "balanceOf", - "outputs": [{"name": "", "type": "uint256"}], + "outputs": [{"name": "", "type": "int256"}], "stateMutability": "view", "type": "function"}] orderbook = w3.eth.contract(address=ORDERBOOK, abi=abi) -USDT = "0x0000000000000000000000000000000000000001" -ACCOUNT = "0xYourAddress" -balance = orderbook.functions.balanceOf(USDT, ACCOUNT).call() +NVDAX = "0x0000000000000000000000000000000000000001" # NVDAx (Tokenized NVIDIA) +balance = orderbook.functions.balanceOf(NVDAX, "0xYourAddress").call() print("Balance:", balance) ``` {% endtab %} @@ -65,7 +63,7 @@ use alloy::{providers::ProviderBuilder, sol}; sol! { #[sol(rpc)] contract Orderbook { - function balanceOf(address token, address account) public view returns (uint256); + function balanceOf(address token, address account) public view returns (int256); } } @@ -79,9 +77,9 @@ async fn main() -> eyre::Result<()> { &provider, ); - let usdt: Address = "0x0000000000000000000000000000000000000001".parse()?; + let nvdax: Address = "0x0000000000000000000000000000000000000001".parse()?; // NVDAx (Tokenized NVIDIA) let account: Address = "0xYourAddress".parse()?; - let balance = orderbook.balanceOf(usdt, account).call().await?; + let balance = orderbook.balanceOf(nvdax, account).call().await?; println!("Balance: {}", balance._0); Ok(()) diff --git a/doc/api-reference/applications-precompiles/orderbook.md b/doc/api-reference/applications-precompiles/orderbook.md index 2b4f531c..3e509011 100644 --- a/doc/api-reference/applications-precompiles/orderbook.md +++ b/doc/api-reference/applications-precompiles/orderbook.md @@ -29,7 +29,7 @@ where `signer` is the order owner, `nonce` is the `submitOrder` transaction's no deadline = ceil((now + LAG) / auction_interval) * auction_interval ``` -`LAG` is the headroom you add to `now` so the intent reaches enough validators before its target batch. It is capped at **10 minutes**; aim for **at least 1 minute** under normal conditions, smaller when you want to target a specific upcoming batch. +`LAG` is the headroom you add to `now` so the intent reaches enough validators before its target batch. Aim for **at least 1 minute** under normal conditions, smaller when you want to target a specific upcoming batch. The alignment rule applies to **every deadline-bearing call** on this precompile — `deposit` and `withdraw` as much as orders, cancels, updates and triggers. All of them pass through the same validator check, so an unaligned deposit deadline is rejected just like an unaligned order deadline. @@ -255,7 +255,9 @@ contract Orderbook { * `ob_getOrders`. This is NOT the `submitOrder` tx hash. * @param newSize The new size for the order. * @param newPrice The new price for the order. - * @param token The token used to cover any additional collateral required by the update. + * @param token Currently unused — the engine ignores this parameter. Spot updates lock any + * additional collateral in the resting order's own token (base for sells, quote for + * buys); perp updates draw on native cross-margin. * @param deadline The Unix timestamp after which this update is invalid in microseconds. Must be a multiple of the market's `auction_interval`. */ function update( diff --git a/doc/api-reference/guides/bridge-from-pod.md b/doc/api-reference/guides/bridge-from-pod.md index 08c12e32..a208f1e4 100644 --- a/doc/api-reference/guides/bridge-from-pod.md +++ b/doc/api-reference/guides/bridge-from-pod.md @@ -61,6 +61,8 @@ const withdrawTx = await podBridge.withdraw(POD_TOKEN, amount, ethRecipient, ETH const receipt = await withdrawTx.wait(); // 2. Get claim proof +// The response is { proof, committee_epoch, aux_tx_suffix }; the byte fields +// (`proof`, `aux_tx_suffix`) arrive as JSON arrays of integers, not hex strings. const claimProof = await podProvider.send("pod_getBridgeClaimProof", [receipt.hash]); // 3. Claim on Ethereum @@ -70,7 +72,9 @@ const ethBridge = new ethers.Contract( ethWallet ); const claimTx = await ethBridge.claim( - ETH_TOKEN, amount, ethRecipient, claimProof.proof, claimProof.auxTxSuffix + ETH_TOKEN, amount, ethRecipient, + Uint8Array.from(claimProof.proof), + Uint8Array.from(claimProof.aux_tx_suffix) ); await claimTx.wait(); ``` @@ -136,12 +140,16 @@ let withdraw_receipt = pod_bridge .get_receipt().await?; // 2. Get claim proof +// The response is { proof, committee_epoch, aux_tx_suffix }; the byte fields +// (`proof`, `aux_tx_suffix`) arrive as JSON arrays of integers, not hex strings. let claim_proof: serde_json::Value = pod_provider .raw_request( "pod_getBridgeClaimProof".into(), vec![withdraw_receipt.transaction_hash], ) .await?; +let proof: Vec = serde_json::from_value(claim_proof["proof"].clone())?; +let aux_tx_suffix: Vec = serde_json::from_value(claim_proof["aux_tx_suffix"].clone())?; // 3. Claim on Ethereum let eth_bridge = EthBridge::new( @@ -153,8 +161,8 @@ eth_bridge eth_token, amount, eth_recipient, - claim_proof["proof"].as_str().unwrap().parse()?, - claim_proof["auxTxSuffix"].as_str().unwrap().parse()?, + proof.into(), + aux_tx_suffix.into(), ) .send().await? .watch().await?; diff --git a/doc/api-reference/guides/bridge-to-pod.md b/doc/api-reference/guides/bridge-to-pod.md index f46a2322..e0aabd23 100644 --- a/doc/api-reference/guides/bridge-to-pod.md +++ b/doc/api-reference/guides/bridge-to-pod.md @@ -20,11 +20,12 @@ const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com"); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); const BRIDGE = "ETHEREUM_BRIDGE_ADDRESS"; -const TOKEN = "TOKEN_ADDRESS"; // use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for native token +const TOKEN = "TOKEN_ADDRESS"; // must be an ERC-20 whitelisted on the bridge (there is no native-token deposit path) const amount = ethers.parseUnits("100", 6); const podRecipient = wallet.address; -// Sign an EIP-2612 permit for gasless approval. +// Sign an EIP-2612 permit for gasless approval. The bridge expects the permit +// as exactly 97 tightly-packed bytes: deadline(32) || v(1) || r(32) || s(32). // If the token does not support permit, set permit to "0x" and // send a separate approval transaction: // const token = new ethers.Contract(TOKEN, ["function approve(address,uint256)"], wallet); @@ -65,11 +66,12 @@ let provider = ProviderBuilder::new() .on_http("https://eth.llamarpc.com".parse()?); let bridge_address = "ETHEREUM_BRIDGE_ADDRESS".parse()?; -let token_address = "TOKEN_ADDRESS".parse()?; // use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for native token +let token_address = "TOKEN_ADDRESS".parse()?; // must be an ERC-20 whitelisted on the bridge (there is no native-token deposit path) let amount = U256::from(100_000_000u64); // e.g. 100 USDC let pod_recipient = signer.address(); -// Sign an EIP-2612 permit for gasless approval. +// Sign an EIP-2612 permit for gasless approval. The bridge expects the permit +// as exactly 97 tightly-packed bytes: deadline(32) || v(1) || r(32) || s(32). // If the token does not support permit, set permit to empty bytes and // send a separate approval transaction first. let permit = vec![]; diff --git a/doc/api-reference/guides/place-a-perpetual-order.md b/doc/api-reference/guides/place-a-perpetual-order.md index a076bb3c..9b2e0ca9 100644 --- a/doc/api-reference/guides/place-a-perpetual-order.md +++ b/doc/api-reference/guides/place-a-perpetual-order.md @@ -2,7 +2,7 @@ This guide walks through opening a leveraged perpetual position on one of Pod's perp markets. For background, see [Perpetuals](https://docs.v2.pod.network/documentation/markets/perpetuals) and [Market Configurations](../market-configurations.md) for the live perp market list. -Perpetual markets are quoted in **USD** and use cross-margin: a single USD deposit serves as collateral for all open perp positions on the account. `size` is the order quantity in **base-asset units** and is signed — positive opens a long, negative opens a short. Margin is computed by the market from `|size| × price / maxLeverage`. +Perpetual markets are quoted in **USD** and use cross-margin: a single USD deposit serves as collateral for all open perp positions on the account. `size` is the order quantity in **base-asset units** and is signed — positive opens a long, negative opens a short. Margin is computed by the market from `|size| × markPrice / maxLeverage` (the mark price, not the order's limit price), adjusted by the mark-to-limit difference. See the [Orderbook precompile reference](../applications-precompiles/orderbook.md) for the timestamp unit, deadline-alignment, and TTL rules that apply to every call below. diff --git a/doc/api-reference/guides/read-market-data.md b/doc/api-reference/guides/read-market-data.md index 59974285..fd0325fe 100644 --- a/doc/api-reference/guides/read-market-data.md +++ b/doc/api-reference/guides/read-market-data.md @@ -13,7 +13,7 @@ const markets = await provider.send("ob_getMarkets", []); ```javascript const orderbookId = "0x0000000000000000000000000000000000000000000000000000000000000001"; // NVDAx-USD spot -const depth = 20; // price levels per side +const depth = 10; // price levels per side (the served snapshot retains at most 10) const snapshot = await provider.send("ob_getOrderbook", [orderbookId, depth]); // Returns: { buys: { price: { volume } }, sells: { ... }, timestamp } @@ -24,9 +24,12 @@ const snapshot = await provider.send("ob_getOrderbook", [orderbookId, depth]); ```javascript const candles = await provider.send("ob_getCandles", [ orderbookId, - startTimestamp, // microseconds - endTimestamp, // microseconds - interval, // candle interval + { + resolution: "1m", // "1m" | "5m" | "15m" | "30m" | "1h" | "4h" | "1d" | "1w" | "1M" + from_ts: startTimestamp, // microseconds + to_ts: endTimestamp, // microseconds, optional + limit: 100, // optional + }, ]); ``` @@ -35,9 +38,11 @@ const candles = await provider.send("ob_getCandles", [ ```javascript const orders = await provider.send("ob_getOrders", [ walletAddress, - { clob_ids: [orderbookId] }, + { orderbook_id: orderbookId }, ]); -// Returns: [{ hash, side, status, price, remainingBase, filledBase, filledQuote, ... }] +// Returns: { orders: [{ order_id, tx_hash, side, status, price, initial_size, +// filled_base_amount, filled_quote_amount, ... }], +// total_count, next_cursor } ``` ## Get positions diff --git a/doc/api-reference/guides/recover-locked-account.md b/doc/api-reference/guides/recover-locked-account.md index dedd9e98..4d21b792 100644 --- a/doc/api-reference/guides/recover-locked-account.md +++ b/doc/api-reference/guides/recover-locked-account.md @@ -22,7 +22,8 @@ const abi = ["function recover(bytes32 txHash, uint64 nonce) public"]; const recovery = new ethers.Contract(RECOVERY, abi, wallet); // 1. Get the recovery target for the locked account -const { txHash: targetTxHash, nonce } = await provider.send("pod_getRecoveryTargetTx", [wallet.address]); +// (the response is `{ hash, nonce }`; it is `null` if the account is not locked) +const { hash: targetTxHash, nonce } = await provider.send("pod_getRecoveryTargetTx", [wallet.address]); // 2. Call the recovery precompile const tx = await recovery.recover(targetTxHash, nonce); @@ -54,13 +55,14 @@ let recovery = Recovery::new( ); // 1. Get the recovery target for the locked account +// (the response is `{ hash, nonce }`; it is `null` if the account is not locked) let target: TargetTx = provider .raw_request("pod_getRecoveryTargetTx".into(), vec![account_address]) .await?; // 2. Call the recovery precompile let receipt = recovery - .recover(target.tx_hash, target.nonce) + .recover(target.hash, target.nonce) .send() .await? .watch() diff --git a/doc/api-reference/json-rpc/README.md b/doc/api-reference/json-rpc/README.md index 1d00eb3a..398d5f1e 100644 --- a/doc/api-reference/json-rpc/README.md +++ b/doc/api-reference/json-rpc/README.md @@ -18,11 +18,11 @@ Pod is not a blockchain and has no blocks. Most `eth_` methods work as expected, | RPC Method | Ethereum | Pod | | ------------------------- | ------------------------------------ | ------------------------------------------------------------ | -| **eth\_blockNumber** | Returns the most recent block number | Returns the latest past perfection timestamp in microseconds | +| **eth\_blockNumber** | Returns the most recent block number | Returns the node's current wall-clock time as a Unix timestamp in seconds | | **eth\_getBlockByHash** | Returns block information by hash | Returns an empty block structure | | **eth\_getBlockByNumber** | Returns block information by number | Returns an empty block structure | -**Timestamps are in microseconds.** Pod uses microsecond-precision Unix timestamps wherever Ethereum uses block numbers - including `eth_blockNumber`, transaction deadlines, and TTLs. +**Timestamps are in microseconds.** Pod uses microsecond-precision Unix timestamps for transaction deadlines and TTLs. Block-number-shaped values are different: `eth_blockNumber` and the `blockNumber` in receipts and block responses carry Unix timestamps in whole **seconds**. **Block-related fields are zeroed.** Since Pod has no blocks, EVM opcodes that reference block properties (`block.number`, `block.coinbase`, `block.difficulty`, `block.basefee`) return 0. `block.timestamp` returns the local validator's timestamp at execution time. diff --git a/doc/api-reference/json-rpc/openapi.yaml b/doc/api-reference/json-rpc/openapi.yaml index 8be1b931..e3e2dc40 100644 --- a/doc/api-reference/json-rpc/openapi.yaml +++ b/doc/api-reference/json-rpc/openapi.yaml @@ -189,7 +189,7 @@ paths: description: | Parameters: 1. `orderbook_id` (bytes32): The 32-byte orderbook identifier - 2. `depth` (integer, optional): Maximum number of price levels to return for each side. If omitted, all available price levels are returned. + 2. `depth` (integer, optional): Maximum number of price levels to return for each side. The served snapshot retains at most 10 price levels per side, so at most 10 levels are returned regardless of `depth`. If omitted, all retained levels (up to 10 per side) are returned. items: oneOf: - $ref: '#/components/schemas/Bytes32' @@ -1203,7 +1203,10 @@ paths: Estimates the gas required for a transaction. **Pod-specific behavior:** If the transaction already specifies a gas value, returns that value. - Otherwise, returns `21000` (standard transaction gas). + Otherwise, the estimate routes through Pod's per-contract gas dispatch using the caller-supplied `from`: + fee-exempt (contract, selector, signer) tuples — e.g. the solver's `submitSolutions` and the relayer's + bridge calls — return `0`, a CLOB `submitBatch` returns `21000 × `, and all other + transactions return the flat `21000`. requestBody: content: application/json: @@ -1703,13 +1706,25 @@ paths: type: array description: | Parameters: - 1. `from_sequence` (integer, nullable): Starting sequence number. None = start from 0 - 2. `to_sequence` (integer, nullable): Ending sequence number. None = up to latest - items: { type: integer, nullable: true } - example: [0, 1000] + 1. `ranges` (array of `[from, to]` pairs): Sequence-number ranges to fetch. Each range is a two-element array `[from, to]`; e.g. `[[0, 1000]]` fetches sequence numbers 0 through 1000. + 2. `binary` (boolean, optional): When `true`, the result is the compact binary encoding, returned as an object `{ "data": "" }`. When `false` or omitted, the result is a plain JSON array of vote batch objects. + items: + oneOf: + - type: array + description: Array of [from, to] sequence-number pairs + items: + type: array + items: { type: integer } + minItems: 2 + maxItems: 2 + - type: boolean + description: Request the compact base64 binary encoding + example: [[[0, 1000]]] responses: '200': - description: Array of vote batch objects + description: | + Array of vote batch objects, or — when `binary` is `true` — an object + `{ "data": "" }` carrying the compact binary encoding. content: application/json: schema: @@ -1717,8 +1732,14 @@ paths: properties: jsonrpc: { type: string } result: - type: array - items: { type: object } + oneOf: + - type: array + items: { type: object } + - type: object + properties: + data: + type: string + description: Base64-encoded compact binary vote batches id: { type: integer } @@ -1762,9 +1783,12 @@ paths: example: jsonrpc: "2.0" result: - signatures: "0x..." + # Byte arrays serialize as JSON arrays of integers, not hex strings. + # `proof` = proof-type byte 0x00 followed by the concatenated 65-byte signatures (truncated here). + proof: [0, 154, 42, 17, 96, 203, 11, 250] committee_epoch: 42 - proof: "0xf74e07ff80dc54c7e894396954326fe13f07d176746a6a29d0ea34922b856402" + # `aux_tx_suffix` = the 32 bytes of the requested tx hash (truncated here). + aux_tx_suffix: [247, 78, 7, 255, 128, 220, 84, 199] id: 1 '400': description: Receipt not found or insufficient attestations @@ -2453,19 +2477,25 @@ components: BridgeClaimProof: type: object - description: Proof data for claiming bridged assets on an external chain via the bridge contract's `claim` function. - required: [signatures, committee_epoch, proof] + description: | + Proof data for claiming bridged assets on an external chain via the bridge contract's `claim` function. + Note that the byte-array fields serialize as JSON arrays of integers (one per byte), not 0x-prefixed hex strings. + required: [proof, committee_epoch, aux_tx_suffix] properties: - signatures: - $ref: '#/components/schemas/HexBytes' - description: Aggregated 65-byte ECDSA signatures (r, s, v) from validators, concatenated + proof: + type: array + items: { type: integer, minimum: 0, maximum: 255 } + description: | + Proof bytes as a JSON array of integers: a proof-type prefix byte (`0x00` = certificate) + followed by the validators' concatenated 65-byte ECDSA signatures (r, s, v) committee_epoch: type: integer format: uint64 description: Committee epoch for signature verification - proof: - $ref: '#/components/schemas/Bytes32' - description: Transaction hash (proof data for the claim) + aux_tx_suffix: + type: array + items: { type: integer, minimum: 0, maximum: 255 } + description: The 32 bytes of the requested transaction hash, as a JSON array of integers HexUint256: type: string @@ -2520,7 +2550,7 @@ components: CandleResolution: type: string - enum: ["1m", "5m", "15m", "1h", "4h", "1d"] + enum: ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M"] description: Time interval for OHLCV candles TriggerType: @@ -2653,6 +2683,9 @@ components: end: $ref: '#/components/schemas/Timestamp' description: Timestamp when order expires (microseconds) + included_batch: + $ref: '#/components/schemas/Timestamp' + description: Deadline of the batch the solver first included this order in — the tick it entered the book, distinct from `deadline` (the signed target). Zero for engine-generated orders and pre-feature rows. effective_price: $ref: '#/components/schemas/HexUint256' description: Effective price the order has been filled at so far (filled_quote / filled_base, 1e18) @@ -2677,6 +2710,10 @@ components: $ref: '#/components/schemas/OrderDirection' description: Composite direction label (set for spot, and for perps once fills land or for liquidation orders). nullable: true + realized_pnl: + type: string + description: Perp only. Aggregate realized PnL (signed, 1e18 scale) across this order's fills — price drift minus funding on every slice it closed. Omitted on spot markets. + nullable: true grouping: $ref: '#/components/schemas/TriggerGrouping' description: Trigger-grouping mode inherited from the parent trigger. Omitted when it is the default `none`. @@ -2704,9 +2741,12 @@ components: low: $ref: '#/components/schemas/HexUint256' description: Lowest price (quote/base with 1e18 scale) - volume: + volume: + $ref: '#/components/schemas/HexUint256' + description: Base volume traded during this candle period (1e18 scale, one-sided) + quote_volume: $ref: '#/components/schemas/HexUint256' - description: Total volume traded during this candle period + description: Quote (USD) notional traded during this candle period (1e18 scale, one-sided). Cannot be derived from `volume` since each tick clears at its own price. Market: type: object @@ -2739,9 +2779,10 @@ components: description: Full name of the quote token market_type: $ref: '#/components/schemas/MarketType' - last_clearing_price: + last_clearing_price: $ref: '#/components/schemas/HexUint256' - description: Last auction clearing price (quote/base with 1e18 scale) + nullable: true + description: Last auction clearing price (quote/base with 1e18 scale). `null` until the market's first fill. auction_interval: type: integer format: int64 @@ -2752,14 +2793,17 @@ components: description: 24-hour trading volume high_24h: $ref: '#/components/schemas/HexUint256' - description: 24-hour highest price + nullable: true + description: 24-hour highest price. `null` until the market has a completed candle (never traded); distinct from a real value of 0. low_24h: $ref: '#/components/schemas/HexUint256' - description: 24-hour lowest price - price_change_24h: + nullable: true + description: 24-hour lowest price. `null` until the market has a completed candle (never traded); distinct from a real value of 0. + price_change_24h: type: integer format: int64 - description: 24-hour price change (signed integer) + nullable: true + description: 24-hour price change (signed integer). `null` until the market has a completed candle (never traded); distinct from a real value of 0. tick_precision: $ref: '#/components/schemas/HexUint256' description: Minimum price tick size, hex-encoded with `1e18` scale (e.g. `0xde0b6b3a7640000` = `1e18` = one whole tick). @@ -3209,6 +3253,9 @@ components: equity: type: string description: Account equity at the moment of the sweep (decimal int256, 1e18). + realized_pnl: + type: string + description: Realized PnL crystallized by the forced close (decimal int256, 1e18). 0 on the terminal cash-sweep row and on pre-migration rows. timestamp: $ref: '#/components/schemas/Timestamp' description: Timestamp of the batch that produced the sweep, in microseconds. @@ -3826,6 +3873,9 @@ components: funding_accrued: type: string description: '`(market.funding − position.entry_funding) × size` (signed, 1e18 USD).' + entry_funding: + type: string + description: Funding accumulator captured at entry (signed, 1e18). With the streamed `funding_index` and the market's funding window, a client can recompute `funding_accrued` live. liquidation_price: $ref: '#/components/schemas/HexUint256' description: Mark price at which this position would be liquidated against its own `margin` and the market's maintenance-margin rate. Funding ignored. 0 when position is empty. @@ -3876,7 +3926,7 @@ components: description: Cash adjusted for unsettled funding plus unrealized PnL across all open perp positions. Excludes spot holdings (signed, 1e18 USD). account_value: type: string - description: '`perps_equity` plus the mark value of all spot holdings (signed, 1e18 USD).' + description: '`perps_equity` plus the mark value of all spot holdings, plus the quote escrowed in resting spot buy orders (signed, 1e18 USD).' cash: type: string description: Deposited collateral adjusted for unsettled funding. Signed — can be negative if the account is underwater (1e18 USD). diff --git a/doc/api-reference/market-configurations.md b/doc/api-reference/market-configurations.md index 5687e2f5..561ea7ad 100644 --- a/doc/api-reference/market-configurations.md +++ b/doc/api-reference/market-configurations.md @@ -15,7 +15,7 @@ Each market on Pod is created with a set of protocol-level parameters that gover | Quote Asset | The settlement currency (e.g. USDC) | | Market Type | `spot` or `perpetual` | | Tick Size | Minimum price increment (1e18) | -| Solver | Public key of the solver responsible for settling batches | +| Solver | Public key of the solver responsible for settling batches — a single global key shared by every market | ## Perpetual Parameters @@ -25,7 +25,7 @@ Each market on Pod is created with a set of protocol-level parameters that gover | Initial Margin | Required margin to open a position. Derived: `1 / Max Leverage` | | Maintenance Margin | Margin floor below which the position becomes eligible for liquidation. Derived: `Initial Margin / 2` | | Interest Rate | Per-market funding constant, defaulting to 0.01% per 8 hours. See [Perpetuals → Funding](../protocol/perpetuals.md) | -| Oracle | Price feed source (Pyth asset) | +| Oracle | Price feed source, specified as `/` (sources: `PythNetwork`, `PythPro`, `Hyperliquid`, `Nobi`) | ## Live Markets @@ -40,7 +40,7 @@ All markets are quoted in USD (the native token, address `0xEeeeeEeeeEeEeeEeEeEe | Batch Interval | 500ms | | Initial Margin | `1 / Max Leverage` | | Maintenance Margin | `0.5 × Initial Margin` | -| Backstop | `0.75 × Initial Margin` | +| Backstop | `(2/3) × Maintenance Margin` (≈ `0.33 × Initial Margin`) | ### Spot Markets diff --git a/doc/protocol/margin.md b/doc/protocol/margin.md index be2d68e0..83da407a 100644 --- a/doc/protocol/margin.md +++ b/doc/protocol/margin.md @@ -6,10 +6,10 @@ Pod uses cross margin. There is a single collateral asset, USD, shared between p Each account is summarized by a handful of quantities. -Start with **cash** - the part of the account not tied to any position's PnL. It accumulates deposits and incoming funding, and decreases on withdrawals and outgoing funding: +Start with **cash** - the part of the account not tied to any open position's unrealized PnL. It accumulates deposits, incoming funding, and PnL realized when a position is reduced, flipped, or closed, and decreases on withdrawals and outgoing funding: ``` -cash = deposits − withdrawals + funding_payments +cash = deposits − withdrawals + funding_payments + realized_pnl ``` (See [Per-position payment](perpetuals.md#per-position-payment) for how funding settles.) @@ -44,10 +44,10 @@ The headroom between equity and `locked_margin` is the **available margin** - ne available_margin = equity − locked_margin ``` -The maximum cash that can be withdrawn at any time is capped both by available margin and by realized cash, since unrealized PnL is not withdrawable until the position closes: +The maximum cash that can be withdrawn at any time is the available margin, floored at zero (and forced to zero when equity falls below `liquidation_margin`): ``` -withdrawable_cash = min(available_margin, cash) +withdrawable_cash = max(0, available_margin) ``` That is the ceiling the account imposes. What actually leaves is further bounded by the bridge: a withdrawal is claimed on another chain, so its amount must be a whole number of that chain's token units and must fall inside the token's `[min, max]`. See [Withdrawals leave Pod](https://docs.v2.pod.network/api-reference/applications-precompiles/orderbook) for the exact rules. @@ -56,7 +56,7 @@ When `equity < liquidation_margin`, positions become eligible for liquidation. ## Liquidation -- Liquidations are submitted as market orders into the order book. They contribute to liquidity and are matched like normal orders. +- Liquidations are placed as reduce-only limit orders at the bankruptcy price. They rest on the book across batches — refreshed as the bankruptcy price drifts, and canceled if the account recovers — so they contribute to liquidity. - If the account's equity falls below 2/3rd of `liquidation_margin`, the entire portfolio is transferred to the backstop vault. - If equity goes negative, auto-deleveraging (ADL) is triggered. @@ -64,6 +64,6 @@ When `equity < liquidation_margin`, positions become eligible for liquidation. When an account's equity is negative, ADL closes positions on the profitable side of the market to cancel out the negative equity: -- Positions on the opposite side are sorted by leverage (highest first). -- These positions are closed at the last batch price until all negative equity is offset. -- The underwater position itself is also closed. +- Positions on the opposite side are ranked by an ADL score — profit ratio × leverage — highest first, with accounts at non-positive equity ranked before all others. +- These positions are closed at the previous batch's mark price until all negative equity is offset. +- The underwater account's losing legs are partially closed just enough to bring its equity back to zero; the remaining portfolio is then swept to the backstop vault. diff --git a/doc/protocol/markets-overview.md b/doc/protocol/markets-overview.md index 0627a8e1..5c0e0740 100644 --- a/doc/protocol/markets-overview.md +++ b/doc/protocol/markets-overview.md @@ -8,11 +8,11 @@ Transactions are added to the network without any central party - there is no le ## Native Markets -Native markets are accessed through the Market precompile. Users deposit funds into the market contract and trade against a central limit order book (CLOB) with batch auction matching. Balances are unified across all native markets - a single deposit can be used for both spot and perpetual trading. +Native markets are accessed through the [Orderbook precompile](orderbook.md). Users deposit funds into the orderbook contract and trade against a central limit order book (CLOB) with batch auction matching. Balances are unified across all native markets - a single deposit can be used for both spot and perpetual trading. ### Batch Settlement -Native markets settle in periodic batches. The batch duration is configurable per market and is expected to be 100-200ms. Within each batch, operations are processed in a fixed sequence: +Native markets settle in periodic batches. The batch duration is a single global setting shared by every market, currently 500ms. Within each batch, operations are processed in a fixed sequence: 1. **Deposits** - all deposit operations are processed first, ensuring funds are available before any trading activity. 2. **Order updates and cancellations** - modifications and cancellations are applied, updating the order book state. diff --git a/doc/protocol/network-architecture/README.md b/doc/protocol/network-architecture/README.md index cf51f026..32bf9742 100644 --- a/doc/protocol/network-architecture/README.md +++ b/doc/protocol/network-architecture/README.md @@ -37,12 +37,12 @@ Full nodes are the entry point to the network. They accept JSON-RPC requests fro ## Validators -Validators form the core of Pod's protocol. Each validator independently receives transactions, validates them, timestamps them, and signs an attestation. Validators do not coordinate with each other before attesting - they respond directly and in parallel. A transaction is final once the client collects attestations from a supermajority (4/5) of the validator set by stake. +Validators form the core of Pod's protocol. Each validator independently receives transactions, validates them, timestamps them, and signs an attestation. Validators do not coordinate with each other before attesting - they respond directly and in parallel. A transaction is final once the client collects attestations from n − f of the n validators in the committee, where f is the number of tolerated faulty validators (the protocol requires n ≥ 5f + 1). Validators also observe deposit events from the native bridge contract on Ethereum and credit balances accordingly. ## Native Bridge -The Pod native bridge is a smart contract deployed on Ethereum. Users deposit ETH or ERC-20 tokens into the bridge contract, which emits deposit events. Validators observe these events and increase the user's balance on Pod. Withdrawals follow the reverse flow - the user initiates a withdrawal on Pod, and once finalized, can claim their tokens from the bridge contract on Ethereum. +The Pod native bridge is a smart contract deployed on Ethereum. Users deposit whitelisted ERC-20 tokens into the bridge contract, which emits deposit events (there is no native-token deposit path). Validators observe these events and increase the user's balance on Pod. Withdrawals follow the reverse flow - the user initiates a withdrawal on Pod, and once finalized, can claim their tokens from the bridge contract on Ethereum. See [Native Bridge](../native-bridge.md) for the full deposit and withdrawal flow. diff --git a/doc/protocol/network-architecture/censorship-resistance.md b/doc/protocol/network-architecture/censorship-resistance.md index f31ba3e6..2d419616 100644 --- a/doc/protocol/network-architecture/censorship-resistance.md +++ b/doc/protocol/network-architecture/censorship-resistance.md @@ -18,5 +18,5 @@ Transactions confirm within one network round trip (2 delta). For a detailed com For time-sensitive applications (e.g. an auction with a deadline), censorship resistance needs a stronger guarantee: not just that a transaction will eventually be included, but that it will be included *before a specific time*. -Pod provides this through [past perfection](timestamping.md#past-perfection). When an application subscribes to a time of interest, the full node returns a past perfect set once that time has been reached. If a transaction was submitted sufficiently before the time of interest (delta before the deadline, where delta is the network delay between the client and the slowest honest validator), it is guaranteed to be in the past perfect set. Validators cannot selectively exclude timely transactions from the set. +Pod provides this through [past perfection](timestamping.md#past-perfection). When an application waits on a time of interest (via `pod_waitPastPerfectTime`), the full node unblocks the call once that time has been reached, and the application can then read the past perfect set - the transactions finalized before it. If a transaction was submitted sufficiently before the time of interest (delta before the deadline, where delta is the network delay between the client and the slowest honest validator), it is guaranteed to be in the past perfect set. Validators cannot selectively exclude timely transactions from the set. diff --git a/doc/protocol/network-architecture/local-ordering.md b/doc/protocol/network-architecture/local-ordering.md index 2588af4b..90bc2435 100644 --- a/doc/protocol/network-architecture/local-ordering.md +++ b/doc/protocol/network-architecture/local-ordering.md @@ -22,9 +22,9 @@ The existing consensusless literature addresses this by falling back to a consen To recover, the client: 1. Calls `pod_getRecoveryTargetTx(account)` on the full node to fetch a valid target transaction to recover to. -2. Sends a transaction to the **recovery precompile** at `0x0000000000000000000000000000000004EC0EE4`, calling `recover(txHash, nonce)`. +2. Sends a transaction to the **recovery precompile** at `0x50d0000000000000000000000000000000000003`, calling `recover(txHash, nonce)`. -The target transaction points to the valid tip of a chain of transactions that can all be finalized. The protocol executes this chain, recovers the account state to the state after executing the target transaction, and increments the nonce. The client can then sign a new transaction with the next nonce (stuck nonce + 1) and continue transacting normally. +The target transaction points to the valid tip of a chain of transactions that can all be finalized. The protocol executes this chain, recovers the account state to the state after executing the target transaction, and increments the nonce. The recovery transaction itself must be signed at a fresh nonce strictly above the stuck nonce (in practice, stuck nonce + 1); once it executes, the client can sign a new transaction with the nonce after the recovery transaction's (recovery nonce + 1) and continue transacting normally. Note that recovery itself is a transaction, so a client can get locked again if it submits multiple conflicting recovery transactions and none of them reach quorum. The protocol handles this - the client simply initiates recovery again, and the new target transaction will account for the full chain including prior recovery attempts. diff --git a/doc/protocol/network-architecture/timestamping.md b/doc/protocol/network-architecture/timestamping.md index f7229798..667eea1b 100644 --- a/doc/protocol/network-architecture/timestamping.md +++ b/doc/protocol/network-architecture/timestamping.md @@ -11,7 +11,7 @@ Both timestamps, combined with the sequence numbers in each validator's temporal ## Past Perfection -Applications can subscribe to a **time of interest** - for example, an auction deadline - using `pod_subscribe`. The time of interest is reached when a quorum (n - f) of validators have signed vote batches with timestamps beyond it. At that point, the full node returns a **past perfect set** associated with that timestamp. +Applications can wait for a **time of interest** - for example, an auction deadline - using the blocking JSON-RPC method `pod_waitPastPerfectTime`. The time of interest is reached when a quorum (n - f) of validators have signed vote batches with timestamps beyond it. The call itself returns an empty result once that happens; the application then fetches the **past perfect set** associated with that timestamp - the transactions finalized before it - separately (e.g. via `eth_getLogs` or `pod_listTransactions`). The past perfect set provides four properties: diff --git a/doc/protocol/network-architecture/transaction-flow.md b/doc/protocol/network-architecture/transaction-flow.md index 7ef79743..0ab70ee0 100644 --- a/doc/protocol/network-architecture/transaction-flow.md +++ b/doc/protocol/network-architecture/transaction-flow.md @@ -50,6 +50,6 @@ The validator broadcasts its attestation to all other validators and full nodes. The full node streams attestations back to the client as they arrive. A transaction is **final** once it has collected **n - f** attestations from the validator set, where **n** is the total number of validators and **f** is the maximum number of Byzantine validators the network tolerates. -With a 5f + 1 validator set, this means a supermajority of 4/5 of validators by stake must attest for finality. Once this threshold is reached, the transaction cannot be reverted - even if the remaining validators are adversarial. +With a validator set at the minimum size n = 5f + 1, this corresponds to 4/5 of the validators attesting for finality (the threshold counts distinct validators - there is no stake weighting). Once this threshold is reached, the transaction cannot be reverted - even if the remaining validators are adversarial. The collection of n - f attestations forms a **finality certificate** that is verifiable outside Pod. Any external system (a smart contract on Ethereum, a TEE enclave, a ZK circuit) can check the certificate to confirm a transaction was finalized on Pod without trusting a relay or intermediary. diff --git a/doc/protocol/orderbook.md b/doc/protocol/orderbook.md index 99620d0a..55f555c0 100644 --- a/doc/protocol/orderbook.md +++ b/doc/protocol/orderbook.md @@ -12,7 +12,7 @@ Orders are immediately added to the order book as soon as they are finalized thr The order book supports limit orders and market orders. The direction of a trade is determined by the sign of the volume parameter - positive for buy/bid, negative for sell/ask. -All markets use 1e18 tick sizes, matching the token decimal standard. +The tick size (minimum price increment) is a per-market parameter; prices must be multiples of it. The default is 1e16 — i.e. 0.01 in the 18-decimal token representation. ### Execution flags @@ -52,7 +52,7 @@ The full node includes a built-in indexer for both live and historical market da Pod uses frequent batch auctions to match orders. Instead of processing orders one at a time as they arrive (continuous trading), orders are collected over a short interval and matched together at a single uniform clearing price. This removes timing-based ordering advantages - competition is on price alone. -Each market has a fixed **batch interval** that defines how often matching rounds run. At the end of every interval the solver settles a batch covering all orders whose `deadline` lands at or before that interval. See [Market Configurations](https://docs.v2.pod.network/guides-references/market-configurations) for the per-market interval on live markets. +The **batch interval** that defines how often matching rounds run is a single global setting shared by every market — currently 500ms. At the end of every interval the solver settles a batch covering all orders whose `deadline` lands at or before that interval. See [Market Configurations](https://docs.v2.pod.network/guides-references/market-configurations) for the live value. ### Clearing @@ -78,7 +78,7 @@ The `deadline` parameter in `submitOrder` specifies the latest batch the user wa deadline = ceil((now + LAG) / auction_interval) * auction_interval ``` -`LAG` is the headroom you give for network and attestation propagation, capped at **10 minutes** in the future from `now_us`. Most integrators should aim for **at least 1 minute**; experts who want to target a specific upcoming batch can push it lower at the risk of missing the batch if the transaction doesn't reach enough validators in time. This 10-minute ceiling is the maximum last look duration and is expected to shorten as the network matures. +`LAG` is the headroom you give for network and attestation propagation. Most integrators should aim for **at least 1 minute**; experts who want to target a specific upcoming batch can push it lower at the risk of missing the batch if the transaction doesn't reach enough validators in time. The protocol guarantees (via [past perfection](network-architecture/timestamping.md#past-perfection)) that if an order receives n - f attestations within the deadline - which it will if it was sent sufficiently early - it will be part of a batch up to and including the latest batch specified by the deadline. @@ -88,7 +88,7 @@ Traders can set the deadline to be small to ensure they are matched quickly, but ### Solver -The solver is the service responsible for settling a batch. It can be a rotating set of solvers or a single entity, configurable per market. The solver waits for the auction deadline and then settles the batch. +The solver is the service responsible for settling a batch. It is currently a single entity: one global solver key, shared by every market. The solver waits for the auction deadline and then settles the batch. The solver does not get any additional advantage. It cannot censor transactions or include transactions that were not submitted in time. It has some flexibility on whether to include out-of-time transactions - orders submitted after the batch timestamp, or orders that received some attestations but fewer than the required n − f. These orders always lose, because they cannot claim funds even if they get matched. diff --git a/doc/protocol/perpetuals.md b/doc/protocol/perpetuals.md index 62fd58ce..8d2b538a 100644 --- a/doc/protocol/perpetuals.md +++ b/doc/protocol/perpetuals.md @@ -12,14 +12,14 @@ It is recomputed every batch: ``` price_diff_ema = clamp(ema(clearing_or_mid − oracle_price, 3 min), 0, oracle_price × max_premium) -mark_price = clamp(oracle_price + price_diff_ema, last_mark_price, mark_clamp_pct × last_mark_price) +mark_price = clamp(oracle_price + price_diff_ema, last_mark_price, mark_price_clamp × last_mark_price) ``` - `clamp(x, center, half_width)` - clips `x` to `[center − half_width, center + half_width]`. - `clearing_or_mid` - the batch's uniform clearing price if it matched, otherwise the order book mid price. - `ema(·, 3 min)` - a 3-minute exponential moving average of the gap between the book price and the oracle. - `max_premium` - a per-market bound; `price_diff_ema` is clamped to `oracle_price × max_premium` so mark cannot drift arbitrarily far from the oracle. -- `mark_clamp_pct` - a per-market bound limiting how far `mark_price` can move from the previous batch's mark price in a single batch, expressed as a fraction of `last_mark_price`. +- `mark_price_clamp` - a per-market bound limiting how far `mark_price` can move from the previous batch's mark price in a single batch, expressed as a fraction of `last_mark_price` (default 1%). ## Funding @@ -43,10 +43,10 @@ funding_rate = clamp( - `impact_bid` / `impact_ask` - the effective price a taker would get when opening a short / long position of size `impact_notional` (a per-market USD amount, defaulting to $10,000) against the current batch auction. - `saturate(x)` - `max(x, 0)`: each term contributes only when it pushes the perp away from spot. - `interest_rate` - a per-market constant, defaulting to 0.01% per 8 hours. -- `max_funding_rate` - a per-market cap on the rate's magnitude, defaulting to 4% per hour. +- `max_funding_rate` - a per-market cap on the rate's magnitude, defaulting to 4% per funding window (8 hours). - `funding_rate` is signed: positive means longs pay shorts, negative means shorts pay longs. -When the perp trades at a premium (`impact_bid` above the oracle) the first `saturate` term is positive and longs pay; when it trades at a discount (`impact_ask` below the oracle) the second term dominates and shorts pay. The rate is computed and applied per batch - `interest_rate` and `max_funding_rate` are shown normalized to 8-hour and hourly figures only for readability. +When the perp trades at a premium (`impact_bid` above the oracle) the first `saturate` term is positive and longs pay; when it trades at a discount (`impact_ask` below the oracle) the second term dominates and shorts pay. The rate is computed and applied per batch - `interest_rate` and `max_funding_rate` are both expressed per `funding_window` (8 hours by default). ### Per-position payment