From 8afcfeccebeb37a41795870be5c5be40ec997179 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Mon, 2 Nov 2026 16:25:41 +0200 Subject: [PATCH 01/11] feat(scenarios): add Jupiter Perps support - state templates --- crates/core/src/scenarios/README.md | 1 + .../protocols/jupiter-perps/doves/v1/idl.json | 849 ++ .../jupiter-perps/doves/v1/overrides.yaml | 56 + .../protocols/jupiter-perps/v1/README.md | 182 + .../protocols/jupiter-perps/v1/idl.json | 8349 +++++++++++++++++ .../protocols/jupiter-perps/v1/overrides.yaml | 239 + crates/core/src/scenarios/registry.rs | 52 +- crates/core/src/tests/jupiter_perps/mod.rs | 1352 +++ crates/core/src/tests/mod.rs | 2 + crates/mcp/src/surfpool/mod.rs | 4 +- 10 files changed, 11080 insertions(+), 6 deletions(-) create mode 100644 crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/idl.json create mode 100644 crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml create mode 100644 crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md create mode 100644 crates/core/src/scenarios/protocols/jupiter-perps/v1/idl.json create mode 100644 crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml create mode 100644 crates/core/src/tests/jupiter_perps/mod.rs diff --git a/crates/core/src/scenarios/README.md b/crates/core/src/scenarios/README.md index 65e2d5683..beb80622d 100644 --- a/crates/core/src/scenarios/README.md +++ b/crates/core/src/scenarios/README.md @@ -21,6 +21,7 @@ Protocols that are natively supported by Surfpool will have their IDLs included - **Pump v1** - Bonding curve launchpad with curve reserve and global config override templates - **Phoenix Eternal** - Perpetuals venue with trader collateral, mark price, maintenance margin, market fee, withdraw limit, trader capability, stop-loss trigger and delegated permission templates. See [protocols/phoenix-eternal/README.md](./protocols/phoenix-eternal/README.md) - **PumpSwap v1** - Constant-product AMM with pool state and global config override templates, including canonical pool derivation for migrated pump.fun coins +- **Jupiter Perps** - Perpetuals exchange and JLP pool with oracle price, custody risk, borrow rate, trading fee, pool limit and pool fee templates, across the Perps and Doves programs. See [protocols/jupiter-perps/v1/README.md](./protocols/jupiter-perps/v1/README.md) For custom protocols, an IDL can be registered at runtime using the [`surfnet_registerIdl`](https://docs.surfpool.run/rpc/cheatcodes#surfnet-registeridl) RPC cheatcode. diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/idl.json b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/idl.json new file mode 100644 index 000000000..cd7acf6fa --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/idl.json @@ -0,0 +1,849 @@ +{ + "accounts": [ + { + "discriminator": [ + 112, + 249, + 139, + 217, + 215, + 208, + 249, + 54 + ], + "name": "AgPriceFeed" + }, + { + "discriminator": [ + 189, + 103, + 252, + 23, + 152, + 35, + 243, + 156 + ], + "name": "PriceFeed" + }, + { + "discriminator": [ + 209, + 117, + 255, + 185, + 196, + 175, + 68, + 9 + ], + "name": "Storage" + } + ], + "address": "DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e", + "errors": [ + { + "code": 6000, + "msg": "Threshold must be >0", + "name": "ThresholdUnderflow" + }, + { + "code": 6001, + "msg": "Threshold must be <= whitelist length", + "name": "ThresholdOverflow" + }, + { + "code": 6002, + "msg": "Number of signers must be >= threshold", + "name": "ThresholdNotMet" + }, + { + "code": 6003, + "msg": "Pair string must not be blank", + "name": "PairStringUnderflow" + }, + { + "code": 6004, + "msg": "Pair string must be <= 32 bytes", + "name": "PairStringOverflow" + }, + { + "code": 6005, + "msg": "Signer out of range", + "name": "SignerIndexOverflow" + }, + { + "code": 6006, + "msg": "Signature verification failed", + "name": "InvalidSigner" + }, + { + "code": 6007, + "msg": "All signers must be unique", + "name": "DuplicateSigner" + }, + { + "code": 6008, + "msg": "New timestamp must be greater than previous one", + "name": "InvalidTimestamp" + }, + { + "code": 6009, + "msg": "Integer overflow", + "name": "Overflow" + }, + { + "code": 6010, + "msg": "Integer underflow", + "name": "Underflow" + }, + { + "code": 6011, + "msg": "Invalid price feed", + "name": "InvalidPriceFeed" + }, + { + "code": 6012, + "msg": "Invalid expo", + "name": "InvalidExpo" + }, + { + "code": 6013, + "msg": "Invalid Pyth Lazer payload", + "name": "InvalidPythLazerPayload" + }, + { + "code": 6014, + "msg": "Invalid Pyth Lazer channel", + "name": "InvalidPythLazerChannel" + }, + { + "code": 6015, + "msg": "Invalid Pyth Lazer price feed accounts", + "name": "InvalidPythLazerFeedAccounts" + }, + { + "code": 6016, + "msg": "Invalid Pyth Lazer config", + "name": "InvalidPythLazerConfig" + }, + { + "code": 6017, + "msg": "Too many Pyth Lazer price feed updates", + "name": "TooManyPythLazerPriceFeedUpdates" + }, + { + "code": 6018, + "msg": "Invalid RedStone price feed accounts", + "name": "InvalidRedstoneFeedAccounts" + }, + { + "code": 6019, + "msg": "Aggregate price feed v2 is already configured", + "name": "AgPriceV2AlreadyConfigured" + } + ], + "instructions": [ + { + "accounts": [ + { + "name": "admin", + "signer": true, + "writable": true + }, + { + "name": "feed", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 100 + ] + }, + { + "kind": "arg", + "path": "pair" + } + ] + }, + "writable": true + }, + { + "address": "11111111111111111111111111111111", + "name": "system_program" + } + ], + "args": [ + { + "name": "pair", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "feed_signer", + "type": { + "array": [ + "u8", + 33 + ] + } + } + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "name": "initialize" + }, + { + "accounts": [ + { + "name": "signer", + "signer": true, + "writable": true + }, + { + "name": "feed", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 100 + ] + }, + { + "kind": "arg", + "path": "pair" + } + ] + }, + "writable": true + }, + { + "address": "11111111111111111111111111111111", + "name": "system_program" + } + ], + "args": [ + { + "name": "pair", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "feed_id", + "type": "u32" + }, + { + "name": "channel_id", + "type": "u8" + } + ], + "discriminator": [ + 244, + 187, + 210, + 9, + 84, + 50, + 255, + 137 + ], + "name": "initialize_pyth_lazer_price_feed" + }, + { + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "ag_price_feed", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 95, + 102, + 101, + 101, + 100 + ] + }, + { + "account": "AgPriceFeed", + "kind": "account", + "path": "ag_price_feed.mint" + } + ] + }, + "writable": true + }, + { + "name": "pyth_lazer_feed" + }, + { + "name": "cl_feed" + }, + { + "name": "redstone_feed" + } + ], + "args": [], + "discriminator": [ + 61, + 65, + 60, + 229, + 235, + 142, + 250, + 117 + ], + "name": "update_ag_price2" + }, + { + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "ag_price_feed", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 95, + 102, + 101, + 101, + 100 + ] + }, + { + "account": "AgPriceFeed", + "kind": "account", + "path": "ag_price_feed.mint" + } + ] + }, + "writable": true + }, + { + "name": "edge_feed" + }, + { + "name": "cl_feed" + }, + { + "name": "pyth_feed" + } + ], + "args": [], + "discriminator": [ + 181, + 230, + 146, + 98, + 116, + 172, + 249, + 27 + ], + "name": "update_ag_price_feed" + }, + { + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "ag_price_feed", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 103, + 95, + 102, + 101, + 101, + 100 + ] + }, + { + "account": "AgPriceFeed", + "kind": "account", + "path": "ag_price_feed.mint" + } + ] + }, + "writable": true + }, + { + "name": "pyth_lazer_feed" + }, + { + "name": "redstone_feed" + } + ], + "args": [], + "discriminator": [ + 166, + 45, + 37, + 18, + 198, + 105, + 232, + 53 + ], + "name": "update_ag_price_feed_to_v2" + }, + { + "accounts": [ + { + "name": "signer", + "signer": true, + "writable": true + }, + { + "address": "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL", + "name": "pyth_lazer_storage" + }, + { + "name": "pyth_lazer_treasury", + "writable": true + }, + { + "address": "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt", + "name": "pyth_lazer_program" + }, + { + "address": "11111111111111111111111111111111", + "name": "system_program" + }, + { + "address": "Sysvar1nstructions1111111111111111111111111", + "name": "instructions_sysvar" + } + ], + "args": [ + { + "name": "message_data", + "type": "bytes" + }, + { + "name": "ed25519_instruction_index", + "type": "u16" + }, + { + "name": "signature_index", + "type": "u8" + }, + { + "name": "raise", + "type": "bool" + } + ], + "discriminator": [ + 203, + 59, + 95, + 176, + 236, + 21, + 73, + 153 + ], + "name": "update_many_with_pyth_lazer" + }, + { + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "feed", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 101, + 101, + 100 + ] + }, + { + "account": "PriceFeed", + "kind": "account", + "path": "feed.pair" + } + ] + }, + "writable": true + }, + { + "address": "11111111111111111111111111111111", + "name": "system_program" + } + ], + "args": [ + { + "name": "update", + "type": { + "defined": { + "name": "UpdateMessage" + } + } + }, + { + "name": "raise", + "type": "bool" + } + ], + "discriminator": [ + 117, + 159, + 82, + 197, + 207, + 85, + 237, + 45 + ], + "name": "update_with_signer" + } + ], + "metadata": { + "description": "Created with Anchor", + "name": "doves", + "spec": "0.1.0", + "version": "0.1.0" + }, + "types": [ + { + "name": "AgPriceFeed", + "type": { + "fields": [ + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "edge_feed", + "type": "pubkey" + }, + { + "name": "cl_feed", + "type": "pubkey" + }, + { + "name": "pyth_feed", + "type": "pubkey" + }, + { + "name": "pyth_feed_id", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i8" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "config", + "type": { + "defined": { + "name": "Config" + } + } + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "pyth_lazer_feed", + "type": "pubkey" + }, + { + "name": "redstone_feed", + "type": "pubkey" + } + ], + "kind": "struct" + } + }, + { + "name": "Config", + "type": { + "fields": [ + { + "name": "max_ag_price_age_sec", + "type": "u32" + }, + { + "name": "max_price_feed_age_sec", + "type": "u32" + }, + { + "name": "max_price_diff_bps", + "type": "u64" + } + ], + "kind": "struct" + } + }, + { + "name": "PriceFeed", + "type": { + "fields": [ + { + "name": "pair", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "signer", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i8" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "pyth_lazer_config", + "type": { + "defined": { + "name": "PythLazerConfig" + } + } + } + ], + "kind": "struct" + } + }, + { + "name": "PythLazerConfig", + "type": { + "fields": [ + { + "name": "feed_id", + "type": "u32" + }, + { + "name": "channel_id", + "type": "u8" + } + ], + "kind": "struct" + } + }, + { + "name": "Storage", + "type": { + "fields": [ + { + "name": "top_authority", + "type": "pubkey" + }, + { + "name": "treasury", + "type": "pubkey" + }, + { + "name": "single_update_fee_in_lamports", + "type": "u64" + }, + { + "name": "num_trusted_signers", + "type": "u8" + }, + { + "name": "trusted_signers", + "type": { + "array": [ + { + "defined": { + "generics": [ + { + "kind": "type", + "type": "pubkey" + } + ], + "name": "TrustedSignerInfo" + } + }, + 5 + ] + } + }, + { + "name": "num_trusted_ecdsa_signers", + "type": "u8" + }, + { + "name": "trusted_ecdsa_signers", + "type": { + "array": [ + { + "defined": { + "generics": [ + { + "kind": "type", + "type": { + "array": [ + "u8", + 20 + ] + } + } + ], + "name": "TrustedSignerInfo" + } + }, + 2 + ] + } + }, + { + "name": "_extra_space", + "type": { + "array": [ + "u8", + 43 + ] + } + } + ], + "kind": "struct" + } + }, + { + "generics": [ + { + "kind": "type", + "name": "T" + } + ], + "name": "TrustedSignerInfo", + "type": { + "fields": [ + { + "name": "pubkey", + "type": { + "generic": "T" + } + }, + { + "name": "expires_at", + "type": "i64" + } + ], + "kind": "struct" + } + }, + { + "name": "UpdateMessage", + "type": { + "fields": [ + { + "name": "recovery_id", + "type": "u8" + }, + { + "name": "signature", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i8" + }, + { + "name": "timestamp", + "type": "i64" + } + ], + "kind": "struct" + } + } + ] +} diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml new file mode 100644 index 000000000..e410c7ef0 --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -0,0 +1,56 @@ +protocol: jupiter-perps-doves +version: v1 +account_type: AgPriceFeed +idl_file_path: idl.json + +tags: + - oracle + - price-feed + - perpetuals + - defi + +templates: + - id: jupiter-perps-doves-price + name: Override Perps Oracle Price + description: Override the Doves price Jupiter Perps reads for one asset + idl_account_name: AgPriceFeed + properties: + - path: price + label: Price + description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8" + address: + type: pubkey + llm_context: | + Jupiter Perps prices every market from a Doves AgPriceFeed, and this template moves that + price. Liquidations, JLP deposits and quotes all read it. Perps never reads the custody's + Pyth account, so pyth-price-feed-v2 does not move Perps. + + FIND THE FEED (read it live, nothing is hardcoded): + 1. getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with + encoding jsonParsed. result.value.data.parsed.custodies lists every custody. + 2. getMultipleAccounts on those custodies with jsonParsed. Take the custody whose mint is + the asset's mint; its doves_ag_oracle is this template's address. + + SET THE PRICE: + - price = usd_price x 10^-expo; read expo from the feed and keep it. + $80.00 -> 8000000000 + - Set fetchBeforeUse: true so the feed's other fields keep their live values. + - Set only the price for a liquidation: liquidations do not check a feed's age. + - Nothing publishes Doves prices on a surfnet. A JLP deposit, withdrawal, swap or quote + reads every custody's feed and refuses one older than 5 seconds (a quote 15), and a + keeper fill needs a price published after its request. For those, also write timestamp + on every feed the action reads, ahead of the Surfnet clock by as long as the test runs: + Perps accepts a time ahead of the clock and records only the clock. + + LIQUIDATIONS: a keeper liquidates a position once collateral + PnL - close and borrow + fees falls below size x 10000 / the custody's pricing.max_leverage. Move a long's price + below its liquidation price, or a short's above it. A cascade is one override per asset + in the same slot. + + JLP PRICE GUARD: a shock that moves the JLP price more than the pool's + max_lp_token_price_change_bps (500 = 5%) refuses JLP deposits (error 6066), so a large shock + on a heavily weighted token such as SOL trips it. Raise the limit with + jupiter-perps-pool-limits. + + EXAMPLE - SOL crashes to $80: + price: 8000000000 diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md new file mode 100644 index 000000000..882dec4d9 --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md @@ -0,0 +1,182 @@ +# Jupiter Perps + +Templates for the Jupiter Perps exchange and the JLP pool behind it, across two programs: + +| Program | Address | Collection | +|---|---|---| +| Jupiter Perps | `PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu` | `jupiter-perps` (this folder) | +| Doves oracle | `DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e` | `jupiter-perps-doves` (`doves/v1`) | + +Perps reads every price from a Doves `AgPriceFeed`. It never reads the custody's Pyth account, so +`pyth-price-feed-v2` does not move Perps prices. + +# Template index + +| Template | Overrides | +|---|---| +| `jupiter-perps-doves-price` | the Doves price Perps reads for one asset | +| `jupiter-perps-custody-risk` | a market's liquidation leverage, size caps and action switches | +| `jupiter-perps-custody-borrow-rate` | the borrow-rate curve a custody charges positions | +| `jupiter-perps-custody-fees` | the fees a market charges to open and close positions | +| `jupiter-perps-pool-limits` | the JLP pool's AUM cap, weight buffer and price guard | +| `jupiter-perps-pool-fees` | the JLP pool's deposit, withdrawal and swap fees | + +## Number formats + +| You'll see | It means | Example | +|---|---|---| +| `_usd` fields, position prices | USD with 6 decimals | $80 -> `80000000` | +| Doves `price` | USD x 10^-expo, expo -8 | $80 -> `8000000000` | +| `_bps` | basis points, 10^4 = 100% | `500` = 5% | +| `pricing.max_leverage` | leverage x 10^4 | `5000000` = 500x | +| `pricing.trade_impact_fee_scalar` | USD with 6 decimals; the price impact fee rate is size / scalar, rounded up to a whole BPS | `1000000000000` charges a $1,000 trade 10 BPS | +| `target_utilization_rate`, `cumulative_interest_rate` | scaled by 10^9 | `800000000` = 80% | + +`limit.max_aum_usd` and `limit.token_weightage_buffer_bps` are `u128`: pass them as decimal strings. + +## Finding a market + +Nothing but the pool is hardcoded; custodies and feeds are read live. + +1. The JLP Pool `5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq` lists its custodies in `custodies`. +2. Each custody holds one token: its `mint`, and its feed in `doves_ag_oracle`. + +A `getAccountInfo` or `getMultipleAccounts` with `jsonParsed` encoding returns these accounts +decoded. Positions live on the custody of the traded token, longs and shorts alike; a short's +collateral sits in the USDC or USDT custody. + +# Recipes + +## Crash a price + +``` +template: jupiter-perps-doves-price address: the custody's doves_ag_oracle +price: 8000000000 # $80 +``` + +Perps refuses a stale feed: a JLP deposit once a feed is the custody's `oracle.max_price_age_sec` +old (5 seconds), a quote once it is three times that, at most 15, and a keeper fill unless the +price was published after the position request. JLP deposits, withdrawals, swaps and quotes read +every custody's feed; positions read their own feeds; liquidations do not check the age. + +## Prices on a fork + +Nothing publishes Doves prices on a surfnet, so a copied feed ages with the Surfnet clock and a +keeper fill finds no price published after its request. For deposits, swaps, quotes and positions, +write `timestamp` on every feed the action reads, ahead of the clock by as long as the test runs. +Perps accepts a time ahead of the clock and records only the clock, so the outcome is the same as +with a fresh price: + +``` +template: jupiter-perps-doves-price address: each feed the action reads +timestamp: +``` + +Keep-alive ([solana-foundation/surfpool#906](https://github.com/solana-foundation/surfpool/pull/906)) +will keep the feeds published once it lands. + +## Make a position liquidatable + +A keeper liquidates a position once `collateral + PnL - close and borrow fees` falls below +`size x 10^4 / pricing.max_leverage`. Three levers reach it without touching the position: + +``` +jupiter-perps-doves-price price: +jupiter-perps-custody-risk pricing.max_leverage: 200000 # liquidate everything above 20x +jupiter-perps-custody-fees decrease_position_bps: +``` + +Liquidation is keeper-only: the deployed program accepts one hardcoded keeper key. On a surfnet +running `--skip-signature-verification`, a transaction can be sent as that keeper. + +A cascade is a price override per asset in the same slot. + +## Charge positions faster + +``` +template: jupiter-perps-custody-borrow-rate +jump_rate_state.min_rate_bps: 100000 # 1000% a year, flat +jump_rate_state.target_rate_bps: 100000 +jump_rate_state.max_rate_bps: 100000 +``` + +The fee accrues over the time since the custody's last update, whenever a transaction touches the +custody. A long pays its own custody's rate; a short's fee follows its +stablecoin collateral custody. + +## Make trading expensive + +``` +template: jupiter-perps-custody-fees address: the market's custody +increase_position_bps: 100 # 1% of size to open +decrease_position_bps: 100 # 1% of size to close or liquidate +``` + +Fees come out of the position's collateral. Closing rounds the fee up. Both directions also pay a +price impact fee of `size x ceil(size x 10^4 / pricing.trade_impact_fee_scalar) / 10^4`. A higher +close fee counts against the margin, so it makes positions liquidatable at an unchanged price. + +## Raise JLP and swap fees + +``` +template: jupiter-perps-pool-fees +fees.add_remove_liquidity_bps: 100 # 1% on deposits and withdrawals +fees.tax_bps: 0 # no weight tax: every deposit pays exactly 1% +``` + +`fees.swap_bps` is the base fee on swaps that involve a non-stable token, and `fees.stable_swap_bps` +on swaps between two stablecoins. A non-stable custody multiplies its swap fee by +`1 + external_swap_fee_multiplier_bps / 10^4`. The weight tax (`fees.tax_bps`, +`fees.stable_swap_tax_bps`) raises the fee of a deposit or swap that moves a token further from its +target weight and lowers it, never below 0, for one that moves it back. +`getAddLiquidityAmountAndFee2` and `getRemoveLiquidityAmountAndFee2` return the fee in BPS. + +## Refuse JLP deposits + +``` +jupiter-perps-pool-limits limit.max_aum_usd: "1" # 6024 +jupiter-perps-custody-risk permissions.allow_add_liquidity: false # 6027, one token +``` + +## Keep JLP open after a crash + +A shock that moves the JLP price more than `max_lp_token_price_change_bps` (500 = 5%) from the +pool's recorded value refuses deposits, so a large shock on a heavily weighted token trips it. + +``` +jupiter-perps-pool-limits max_lp_token_price_change_bps: 10000 +``` + +A large crash also shifts every token's weight; raise `limit.token_weightage_buffer_bps` if +deposits then fail with 6043. + +# Troubleshooting + +| Error | Meaning | Fix | +|---|---|---| +| 6003 `StaleOraclePrice` | a feed the instruction read is too old: 5 seconds for a deposit, 15 for a quote | write `timestamp` ahead of the clock on every feed of the pool - see "Prices on a fork" | +| 6009 `InvalidPositionState` | the position is not liquidatable | move the price further, or lower `pricing.max_leverage` | +| 6014 `InvalidCustodyBalance` | a custody's vault and staked lamports hold less than its `assets.owned`: the two were copied from mainnet at different moments, as when Studio's market list or a lookup read the custody first, or `fetchBeforeUse` reloaded only the custody | `surfnet_resetAccount` on the custody and on its `token_account`; the next transaction loads them together | +| 6022 `MaxLeverage` | the position would open past its leverage limit, as when its close fee exceeds its collateral | lower `decrease_position_bps` or add collateral | +| 6024 `PoolAmountLimit` | the deposit would exceed `limit.max_aum_usd` | raise the cap | +| 6027 `InstructionNotAllowed` | a `permissions` switch is off | set it back to `true` | +| 6034 `InvalidKeeper` | the liquidator is not the keeper the program accepts | sign as the keeper | +| 6043 `ExceedTokenWeightage` | the deposit leaves the token too far above its target weight | raise `limit.token_weightage_buffer_bps` | +| 6045 `PullOraclePublishTimeTooEarly` | a keeper filled a position request with a price published no later than the request | write the position's feed `timestamp` later than the request - see "Prices on a fork" | +| 6066 `LpTokenPriceChangeLimitExceeded` | the JLP price moved past the guard | raise `max_lp_token_price_change_bps` | +| 6000 `MathOverflow` on a stablecoin custody | its borrow-lend debt overflowed a large rate or time jump | lower the rate or the time step | + +# IDL provenance + +Perps publishes a legacy Anchor IDL on chain, in account `38GK1i4cQPAxqrbfKX4RRMNfXpKRn5PgLFHVJeXm1C8Y`, +which the engine cannot read. `idl.json` is that IDL after `anchor idl convert` (anchor-cli +0.31.1), unchanged. + +| File | sha256 | +|---|---| +| on-chain legacy IDL | `1292d2ecec3f6c17501704a423f1e0f4c2cde25b7821cabeeb2b98eb52168861` | +| `idl.json` | `8d9860d8baf41478d8a44f9507611043ffa685c8d1e801ee032c16817a46b3f3` | + +Doves publishes a current Anchor IDL in account `4D2RLxRVWjGUp7E45noWC6x7BfmSQbQ4mQevHvTV5ai6`. +`doves/v1/idl.json` is that IDL as fetched, unchanged (sha256 +`4945d1b7f0782cf0221e0fc9953ec2f8560604ea8aeb79fd76962a2417e71b2c`). diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/idl.json b/crates/core/src/scenarios/protocols/jupiter-perps/v1/idl.json new file mode 100644 index 000000000..963da4daf --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/idl.json @@ -0,0 +1,8349 @@ +{ + "address": "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu", + "metadata": { + "name": "perpetuals", + "version": "0.1.0", + "spec": "0.1.0" + }, + "instructions": [ + { + "name": "init", + "discriminator": [ + 220, + 59, + 207, + 236, + 108, + 250, + 47, + 100 + ], + "accounts": [ + { + "name": "upgrade_authority", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "transfer_authority", + "writable": true + }, + { + "name": "perpetuals", + "writable": true + }, + { + "name": "perpetuals_program" + }, + { + "name": "perpetuals_program_data" + }, + { + "name": "system_program" + }, + { + "name": "token_program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitParams" + } + } + } + ] + }, + { + "name": "add_pool", + "discriminator": [ + 115, + 230, + 212, + 211, + 175, + 49, + 39, + 169 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals", + "writable": true + }, + { + "name": "pool", + "writable": true + }, + { + "name": "lp_token_mint", + "writable": true + }, + { + "name": "system_program" + }, + { + "name": "token_program" + }, + { + "name": "rent" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AddPoolParams" + } + } + } + ] + }, + { + "name": "add_custody", + "discriminator": [ + 247, + 254, + 126, + 17, + 26, + 6, + 215, + 117 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "custody_token_mint" + }, + { + "name": "system_program" + }, + { + "name": "token_program" + }, + { + "name": "rent" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AddCustodyParams" + } + } + } + ] + }, + { + "name": "set_custody_config", + "discriminator": [ + 133, + 97, + 130, + 143, + 215, + 229, + 36, + 176 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "custody", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetCustodyConfigParams" + } + } + } + ] + }, + { + "name": "set_pool_config", + "discriminator": [ + 216, + 87, + 65, + 125, + 113, + 110, + 185, + 120 + ], + "accounts": [ + { + "name": "admin", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetPoolConfigParams" + } + } + } + ] + }, + { + "name": "set_perpetuals_config", + "discriminator": [ + 80, + 72, + 21, + 191, + 29, + 121, + 45, + 111 + ], + "accounts": [ + { + "name": "admin", + "signer": true + }, + { + "name": "perpetuals", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetPerpetualsConfigParams" + } + } + } + ] + }, + { + "name": "transfer_admin", + "discriminator": [ + 42, + 242, + 66, + 106, + 228, + 10, + 111, + 156 + ], + "accounts": [ + { + "name": "admin", + "signer": true + }, + { + "name": "new_admin" + }, + { + "name": "perpetuals", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TransferAdminParams" + } + } + } + ] + }, + { + "name": "withdraw_fees2", + "discriminator": [ + 252, + 128, + 143, + 145, + 225, + 221, + 159, + 207 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "receiving_token_account", + "writable": true + }, + { + "name": "token_program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawFees2Params" + } + } + } + ] + }, + { + "name": "create_token_metadata", + "discriminator": [ + 221, + 80, + 176, + 37, + 153, + 188, + 160, + 68 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "transfer_authority" + }, + { + "name": "metadata", + "writable": true + }, + { + "name": "lp_token_mint" + }, + { + "name": "token_metadata_program" + }, + { + "name": "system_program" + }, + { + "name": "rent" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateTokenMetadataParams" + } + } + } + ] + }, + { + "name": "create_token_ledger", + "discriminator": [ + 232, + 242, + 197, + 253, + 240, + 143, + 129, + 52 + ], + "accounts": [ + { + "name": "token_ledger", + "writable": true, + "signer": true + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [] + }, + { + "name": "realloc_custody", + "discriminator": [ + 123, + 58, + 109, + 139, + 133, + 7, + 225, + 200 + ], + "accounts": [ + { + "name": "keeper", + "writable": true, + "signer": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "system_program" + }, + { + "name": "rent" + } + ], + "args": [] + }, + { + "name": "realloc_pool", + "discriminator": [ + 114, + 128, + 37, + 167, + 71, + 227, + 40, + 178 + ], + "accounts": [ + { + "name": "keeper", + "writable": true, + "signer": true + }, + { + "name": "pool", + "writable": true + }, + { + "name": "system_program" + }, + { + "name": "rent" + } + ], + "args": [] + }, + { + "name": "create_and_delegate_stake_account", + "discriminator": [ + 98, + 209, + 122, + 27, + 222, + 137, + 94, + 134 + ], + "accounts": [ + { + "name": "keeper", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "stake_account", + "writable": true + }, + { + "name": "stake_info", + "writable": true + }, + { + "name": "validator_vote_account" + }, + { + "name": "stake_config" + }, + { + "name": "wsol_mint" + }, + { + "name": "temp_wsol_account", + "writable": true + }, + { + "name": "rent" + }, + { + "name": "clock" + }, + { + "name": "stake_history" + }, + { + "name": "stake_program" + }, + { + "name": "system_program" + }, + { + "name": "token_program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateAndDelegateStakeAccountParams" + } + } + } + ] + }, + { + "name": "unstake", + "discriminator": [ + 90, + 95, + 107, + 42, + 205, + 124, + 50, + 225 + ], + "accounts": [ + { + "name": "operator", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody" + }, + { + "name": "transfer_authority" + }, + { + "name": "stake_account", + "writable": true + }, + { + "name": "stake_info", + "writable": true + }, + { + "name": "clock" + }, + { + "name": "stake_program" + } + ], + "args": [] + }, + { + "name": "withdraw_stake", + "discriminator": [ + 153, + 8, + 22, + 138, + 105, + 176, + 87, + 66 + ], + "accounts": [ + { + "name": "keeper", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "transfer_authority", + "writable": true + }, + { + "name": "stake_account", + "writable": true + }, + { + "name": "stake_info", + "writable": true + }, + { + "name": "clock" + }, + { + "name": "stake_history" + }, + { + "name": "stake_program" + }, + { + "name": "system_program" + }, + { + "name": "token_program" + } + ], + "args": [] + }, + { + "name": "redeem_stake", + "discriminator": [ + 178, + 203, + 250, + 105, + 133, + 118, + 255, + 69 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "stake_account" + }, + { + "name": "stake_info", + "writable": true + } + ], + "args": [] + }, + { + "name": "operator_set_custody_config", + "discriminator": [ + 166, + 137, + 92, + 204, + 145, + 224, + 24, + 218 + ], + "accounts": [ + { + "name": "operator", + "signer": true + }, + { + "name": "custody", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "OperatorSetCustodyConfigParams" + } + } + } + ] + }, + { + "name": "operator_set_pool_config", + "discriminator": [ + 76, + 201, + 80, + 18, + 199, + 92, + 246, + 105 + ], + "accounts": [ + { + "name": "operator", + "signer": true + }, + { + "name": "pool", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "OperatorSetPoolConfigParams" + } + } + } + ] + }, + { + "name": "test_init", + "discriminator": [ + 48, + 51, + 92, + 122, + 81, + 19, + 112, + 41 + ], + "accounts": [ + { + "name": "upgrade_authority", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "transfer_authority", + "writable": true + }, + { + "name": "perpetuals", + "writable": true + }, + { + "name": "system_program" + }, + { + "name": "token_program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TestInitParams" + } + } + } + ] + }, + { + "name": "set_test_time", + "discriminator": [ + 242, + 231, + 177, + 251, + 126, + 145, + 159, + 104 + ], + "accounts": [ + { + "name": "admin", + "signer": true + }, + { + "name": "perpetuals", + "writable": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetTestTimeParams" + } + } + } + ] + }, + { + "name": "set_token_ledger", + "discriminator": [ + 228, + 85, + 185, + 112, + 78, + 79, + 77, + 2 + ], + "accounts": [ + { + "name": "token_ledger", + "writable": true + }, + { + "name": "token_account" + }, + { + "name": "token_program" + } + ], + "args": [] + }, + { + "name": "swap2", + "discriminator": [ + 65, + 75, + 63, + 76, + 235, + 91, + 91, + 136 + ], + "accounts": [ + { + "name": "owner", + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "receiving_custody", + "writable": true + }, + { + "name": "receiving_custody_doves_price_account" + }, + { + "name": "receiving_custody_pythnet_price_account" + }, + { + "name": "receiving_custody_token_account", + "writable": true + }, + { + "name": "dispensing_custody", + "writable": true + }, + { + "name": "dispensing_custody_doves_price_account" + }, + { + "name": "dispensing_custody_pythnet_price_account" + }, + { + "name": "dispensing_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "Swap2Params" + } + } + } + ] + }, + { + "name": "swap_with_token_ledger", + "discriminator": [ + 139, + 141, + 238, + 197, + 41, + 211, + 172, + 19 + ], + "accounts": [ + { + "name": "owner", + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "receiving_custody", + "writable": true + }, + { + "name": "receiving_custody_doves_price_account" + }, + { + "name": "receiving_custody_token_account", + "writable": true + }, + { + "name": "dispensing_custody", + "writable": true + }, + { + "name": "dispensing_custody_doves_price_account" + }, + { + "name": "dispensing_custody_token_account", + "writable": true + }, + { + "name": "token_ledger" + }, + { + "name": "token_program" + }, + { + "name": "instruction" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SwapWithTokenLedgerParams" + } + } + } + ] + }, + { + "name": "instant_increase_position_pre_swap", + "discriminator": [ + 197, + 38, + 86, + 165, + 199, + 23, + 38, + 234 + ], + "accounts": [ + { + "name": "owner", + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "receiving_custody", + "writable": true + }, + { + "name": "receiving_custody_doves_price_account" + }, + { + "name": "receiving_custody_token_account", + "writable": true + }, + { + "name": "dispensing_custody", + "writable": true + }, + { + "name": "dispensing_custody_doves_price_account" + }, + { + "name": "dispensing_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "instruction" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantIncreasePositionPreSwapParams" + } + } + } + ] + }, + { + "name": "add_liquidity2", + "discriminator": [ + 228, + 162, + 78, + 28, + 70, + 219, + 116, + 115 + ], + "accounts": [ + { + "name": "owner", + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "lp_token_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "lp_token_mint", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AddLiquidity2Params" + } + } + } + ] + }, + { + "name": "remove_liquidity2", + "discriminator": [ + 230, + 215, + 82, + 127, + 241, + 101, + 227, + 146 + ], + "accounts": [ + { + "name": "owner", + "signer": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "lp_token_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "lp_token_mint", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RemoveLiquidity2Params" + } + } + } + ] + }, + { + "name": "create_increase_position_market_request", + "discriminator": [ + 184, + 85, + 199, + 24, + 105, + 171, + 156, + 56 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "collateral_custody" + }, + { + "name": "input_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateIncreasePositionMarketRequestParams" + } + } + } + ] + }, + { + "name": "create_decrease_position_request2", + "discriminator": [ + 105, + 64, + 201, + 82, + 250, + 14, + 109, + 77 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position" + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody" + }, + { + "name": "desired_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateDecreasePositionRequest2Params" + } + } + } + ] + }, + { + "name": "create_decrease_position_market_request", + "discriminator": [ + 74, + 198, + 195, + 86, + 193, + 99, + 1, + 79 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position" + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "collateral_custody" + }, + { + "name": "desired_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateDecreasePositionMarketRequestParams" + } + } + } + ] + }, + { + "name": "update_decrease_position_request2", + "discriminator": [ + 144, + 200, + 249, + 255, + 108, + 217, + 249, + 116 + ], + "accounts": [ + { + "name": "owner", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position" + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateDecreasePositionRequest2Params" + } + } + } + ] + }, + { + "name": "close_position_request2", + "discriminator": [ + 121, + 68, + 162, + 28, + 216, + 47, + 200, + 66 + ], + "accounts": [ + { + "name": "keeper", + "writable": true, + "signer": true, + "optional": true + }, + { + "name": "owner", + "writable": true + }, + { + "name": "owner_ata", + "writable": true + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position" + }, + { + "name": "mint" + }, + { + "name": "token_program" + }, + { + "name": "system_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "close_position_request3", + "discriminator": [ + 122, + 130, + 33, + 18, + 211, + 44, + 161, + 58 + ], + "accounts": [ + { + "name": "keeper", + "writable": true, + "signer": true, + "optional": true + }, + { + "name": "owner", + "writable": true + }, + { + "name": "owner_ata", + "writable": true + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position" + }, + { + "name": "custody", + "writable": true + }, + { + "name": "mint" + }, + { + "name": "token_program" + }, + { + "name": "system_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "increase_position4", + "discriminator": [ + 67, + 147, + 53, + 23, + 43, + 57, + 16, + 67 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "IncreasePosition4Params" + } + } + } + ] + }, + { + "name": "increase_position_pre_swap", + "discriminator": [ + 26, + 136, + 225, + 217, + 22, + 21, + 83, + 20 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "keeper_ata", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position" + }, + { + "name": "collateral_custody" + }, + { + "name": "collateral_custody_token_account" + }, + { + "name": "instruction" + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "IncreasePositionPreSwapParams" + } + } + } + ] + }, + { + "name": "increase_position_with_internal_swap", + "discriminator": [ + 114, + 55, + 106, + 140, + 199, + 221, + 32, + 112 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "receiving_custody", + "writable": true + }, + { + "name": "receiving_custody_doves_price_account" + }, + { + "name": "receiving_custody_pythnet_price_account" + }, + { + "name": "receiving_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "IncreasePositionWithInternalSwapParams" + } + } + } + ] + }, + { + "name": "decrease_position4", + "discriminator": [ + 185, + 161, + 114, + 175, + 96, + 148, + 3, + 170 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "owner", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DecreasePosition4Params" + } + } + } + ] + }, + { + "name": "decrease_position_with_internal_swap", + "discriminator": [ + 131, + 17, + 153, + 110, + 119, + 100, + 97, + 38 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "owner", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "dispensing_custody", + "writable": true + }, + { + "name": "dispensing_custody_doves_price_account" + }, + { + "name": "dispensing_custody_pythnet_price_account" + }, + { + "name": "dispensing_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DecreasePositionWithInternalSwapParams" + } + } + } + ] + }, + { + "name": "decrease_position_with_tpsl", + "discriminator": [ + 108, + 18, + 203, + 209, + 227, + 103, + 65, + 165 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "owner" + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DecreasePositionWithTpslParams" + } + } + } + ] + }, + { + "name": "decrease_position_with_tpsl_and_internal_swap", + "discriminator": [ + 2, + 111, + 200, + 231, + 35, + 65, + 123, + 235 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "owner" + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "dispensing_custody", + "writable": true + }, + { + "name": "dispensing_custody_doves_price_account" + }, + { + "name": "dispensing_custody_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DecreasePositionWithTpslAndInternalSwapParams" + } + } + } + ] + }, + { + "name": "liquidate_full_position4", + "discriminator": [ + 64, + 176, + 88, + 51, + 168, + 188, + 156, + 175 + ], + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidateFullPosition4Params" + } + } + } + ] + }, + { + "name": "refresh_assets_under_management", + "discriminator": [ + 162, + 0, + 215, + 55, + 225, + 15, + 185, + 0 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "lp_token_mint" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RefreshAssetsUnderManagementParams" + } + } + } + ] + }, + { + "name": "set_max_global_sizes", + "discriminator": [ + 89, + 2, + 210, + 24, + 167, + 227, + 13, + 214 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "pool" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetMaxGlobalSizesParams" + } + } + } + ] + }, + { + "name": "instant_create_tpsl", + "discriminator": [ + 117, + 98, + 66, + 127, + 30, + 50, + 73, + 185 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position" + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody" + }, + { + "name": "desired_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantCreateTpslParams" + } + } + } + ] + }, + { + "name": "instant_create_limit_order", + "discriminator": [ + 194, + 37, + 195, + 123, + 40, + 127, + 126, + 156 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position", + "writable": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody" + }, + { + "name": "input_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantCreateLimitOrderParams" + } + } + } + ] + }, + { + "name": "instant_increase_position", + "discriminator": [ + 164, + 126, + 68, + 182, + 223, + 166, + 64, + 183 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "funding_account", + "writable": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "token_ledger", + "optional": true + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantIncreasePositionParams" + } + } + } + ] + }, + { + "name": "instant_decrease_position", + "discriminator": [ + 46, + 23, + 240, + 44, + 30, + 138, + 94, + 140 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_pythnet_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "desired_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantDecreasePositionParams" + } + } + } + ] + }, + { + "name": "instant_decrease_position2", + "discriminator": [ + 162, + 191, + 200, + 62, + 139, + 62, + 176, + 17 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "receiving_account", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "position", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "collateral_custody", + "writable": true + }, + { + "name": "collateral_custody_doves_price_account" + }, + { + "name": "collateral_custody_token_account", + "writable": true + }, + { + "name": "desired_mint" + }, + { + "name": "referral", + "optional": true + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "position_request_ata", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantDecreasePosition2Params" + } + } + } + ] + }, + { + "name": "instant_update_limit_order", + "discriminator": [ + 136, + 245, + 229, + 58, + 121, + 141, + 12, + 207 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position" + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantUpdateLimitOrderParams" + } + } + } + ] + }, + { + "name": "instant_update_tpsl", + "discriminator": [ + 144, + 228, + 114, + 37, + 165, + 242, + 111, + 101 + ], + "accounts": [ + { + "name": "keeper", + "signer": true + }, + { + "name": "api_keeper", + "signer": true + }, + { + "name": "owner", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "position" + }, + { + "name": "position_request", + "writable": true + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InstantUpdateTpslParams" + } + } + } + ] + }, + { + "name": "get_add_liquidity_amount_and_fee2", + "discriminator": [ + 109, + 157, + 55, + 169, + 8, + 81, + 4, + 118 + ], + "accounts": [ + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "lp_token_mint" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "GetAddLiquidityAmountAndFee2Params" + } + } + } + ], + "returns": { + "defined": { + "name": "AmountAndFee" + } + } + }, + { + "name": "get_remove_liquidity_amount_and_fee2", + "discriminator": [ + 183, + 59, + 72, + 110, + 223, + 243, + 150, + 142 + ], + "accounts": [ + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody" + }, + { + "name": "custody_doves_price_account" + }, + { + "name": "custody_pythnet_price_account" + }, + { + "name": "lp_token_mint" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "GetRemoveLiquidityAmountAndFee2Params" + } + } + } + ], + "returns": { + "defined": { + "name": "AmountAndFee" + } + } + }, + { + "name": "get_assets_under_management2", + "discriminator": [ + 193, + 210, + 13, + 249, + 113, + 149, + 29, + 84 + ], + "accounts": [ + { + "name": "perpetuals" + }, + { + "name": "pool" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "GetAssetsUnderManagement2Params" + } + } + } + ], + "returns": "u128" + }, + { + "name": "borrow_from_custody", + "discriminator": [ + 153, + 183, + 65, + 65, + 113, + 33, + 249, + 45 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "user_token_account", + "writable": true + }, + { + "name": "lp_token_mint" + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "BorrowFromCustodyParams" + } + } + } + ] + }, + { + "name": "repay_to_custody", + "discriminator": [ + 211, + 219, + 183, + 222, + 248, + 74, + 5, + 26 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody", + "writable": true + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "custody_token_account", + "writable": true + }, + { + "name": "user_token_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RepayToCustodyParams" + } + } + } + ] + }, + { + "name": "deposit_collateral_for_borrows", + "discriminator": [ + 17, + 2, + 195, + 190, + 76, + 16, + 238, + 74 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody" + }, + { + "name": "transfer_authority" + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "collateral_token_account", + "writable": true + }, + { + "name": "user_token_account", + "writable": true + }, + { + "name": "lp_token_mint" + }, + { + "name": "token_program" + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DepositParams" + } + } + } + ] + }, + { + "name": "withdraw_collateral_for_borrows", + "discriminator": [ + 117, + 160, + 60, + 82, + 237, + 233, + 46, + 182 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool" + }, + { + "name": "custody", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "collateral_token_account", + "writable": true + }, + { + "name": "user_token_account", + "writable": true + }, + { + "name": "lp_token_mint" + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawParams" + } + } + } + ] + }, + { + "name": "liquidate_borrow_position", + "discriminator": [ + 235, + 201, + 17, + 133, + 234, + 72, + 84, + 210 + ], + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "collateral_token_account", + "writable": true + }, + { + "name": "lp_token_mint", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidateBorrowPositionParams" + } + } + } + ] + }, + { + "name": "partial_liquidate_borrow_position", + "discriminator": [ + 250, + 166, + 13, + 74, + 97, + 204, + 130, + 209 + ], + "accounts": [ + { + "name": "signer", + "signer": true + }, + { + "name": "perpetuals" + }, + { + "name": "pool", + "writable": true + }, + { + "name": "custody", + "writable": true + }, + { + "name": "transfer_authority" + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "collateral_token_account", + "writable": true + }, + { + "name": "lp_token_mint", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "PartialLiquidateBorrowPositionParams" + } + } + } + ] + }, + { + "name": "close_borrow_position", + "discriminator": [ + 204, + 226, + 145, + 205, + 232, + 37, + 3, + 140 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "borrow_position", + "writable": true + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CloseBorrowPositionParams" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "BorrowPosition", + "discriminator": [ + 243, + 140, + 20, + 139, + 32, + 243, + 114, + 55 + ] + }, + { + "name": "Custody", + "discriminator": [ + 1, + 184, + 48, + 81, + 93, + 131, + 63, + 145 + ] + }, + { + "name": "Perpetuals", + "discriminator": [ + 28, + 167, + 98, + 191, + 104, + 82, + 108, + 196 + ] + }, + { + "name": "Pool", + "discriminator": [ + 241, + 154, + 109, + 4, + 17, + 177, + 109, + 188 + ] + }, + { + "name": "PositionRequest", + "discriminator": [ + 12, + 38, + 250, + 199, + 46, + 154, + 32, + 216 + ] + }, + { + "name": "Position", + "discriminator": [ + 170, + 188, + 143, + 228, + 122, + 64, + 247, + 208 + ] + }, + { + "name": "StakeInfo", + "discriminator": [ + 66, + 62, + 68, + 70, + 108, + 179, + 183, + 235 + ] + }, + { + "name": "TokenLedger", + "discriminator": [ + 156, + 247, + 9, + 188, + 54, + 108, + 85, + 77 + ] + } + ], + "events": [ + { + "name": "CreatePositionRequestEvent", + "discriminator": [ + 2, + 238, + 94, + 53, + 105, + 211, + 46, + 186 + ] + }, + { + "name": "InstantCreateTpslEvent", + "discriminator": [ + 242, + 54, + 6, + 95, + 24, + 141, + 103, + 198 + ] + }, + { + "name": "InstantUpdateTpslEvent", + "discriminator": [ + 177, + 22, + 47, + 37, + 120, + 246, + 17, + 101 + ] + }, + { + "name": "ClosePositionRequestEvent", + "discriminator": [ + 21, + 34, + 92, + 158, + 224, + 29, + 180, + 243 + ] + }, + { + "name": "IncreasePositionEvent", + "discriminator": [ + 245, + 113, + 85, + 52, + 214, + 187, + 153, + 132 + ] + }, + { + "name": "IncreasePositionPreSwapEvent", + "discriminator": [ + 237, + 107, + 9, + 139, + 22, + 75, + 4, + 213 + ] + }, + { + "name": "DecreasePositionEvent", + "discriminator": [ + 64, + 156, + 43, + 74, + 109, + 131, + 16, + 127 + ] + }, + { + "name": "DecreasePositionPostSwapEvent", + "discriminator": [ + 23, + 210, + 16, + 233, + 98, + 245, + 89, + 82 + ] + }, + { + "name": "LiquidateFullPositionEvent", + "discriminator": [ + 128, + 101, + 71, + 168, + 128, + 72, + 86, + 84 + ] + }, + { + "name": "PoolSwapEvent", + "discriminator": [ + 40, + 107, + 212, + 26, + 223, + 136, + 39, + 220 + ] + }, + { + "name": "PoolSwapExactOutEvent", + "discriminator": [ + 121, + 118, + 11, + 11, + 198, + 66, + 142, + 115 + ] + }, + { + "name": "AddLiquidityEvent", + "discriminator": [ + 27, + 178, + 153, + 186, + 47, + 196, + 140, + 45 + ] + }, + { + "name": "RemoveLiquidityEvent", + "discriminator": [ + 141, + 199, + 182, + 123, + 159, + 94, + 215, + 102 + ] + }, + { + "name": "InstantCreateLimitOrderEvent", + "discriminator": [ + 10, + 163, + 85, + 115, + 129, + 224, + 80, + 192 + ] + }, + { + "name": "InstantIncreasePositionEvent", + "discriminator": [ + 205, + 236, + 57, + 4, + 209, + 106, + 87, + 69 + ] + }, + { + "name": "InstantDecreasePositionEvent", + "discriminator": [ + 171, + 173, + 106, + 25, + 239, + 190, + 58, + 59 + ] + }, + { + "name": "DepositCollateralEvent", + "discriminator": [ + 169, + 14, + 102, + 148, + 155, + 137, + 18, + 235 + ] + }, + { + "name": "WithdrawCollateralEvent", + "discriminator": [ + 145, + 38, + 46, + 87, + 190, + 149, + 253, + 191 + ] + }, + { + "name": "BorrowFromCustodyEvent", + "discriminator": [ + 23, + 121, + 131, + 68, + 168, + 70, + 14, + 76 + ] + }, + { + "name": "RepayToCustodyEvent", + "discriminator": [ + 232, + 54, + 116, + 175, + 186, + 24, + 249, + 221 + ] + }, + { + "name": "LiquidateBorrowPositionEvent", + "discriminator": [ + 11, + 128, + 252, + 59, + 49, + 192, + 56, + 170 + ] + }, + { + "name": "RedeemStakeEvent", + "discriminator": [ + 0, + 241, + 84, + 141, + 139, + 170, + 218, + 110 + ] + }, + { + "name": "WithdrawStakeEvent", + "discriminator": [ + 47, + 85, + 239, + 214, + 207, + 29, + 151, + 88 + ] + }, + { + "name": "DelegateStakeEvent", + "discriminator": [ + 85, + 135, + 75, + 222, + 168, + 133, + 159, + 212 + ] + }, + { + "name": "WithdrawFeesEvent", + "discriminator": [ + 236, + 118, + 138, + 90, + 139, + 173, + 177, + 89 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "MathOverflow", + "msg": "Overflow in arithmetic operation" + }, + { + "code": 6001, + "name": "UnsupportedOracle", + "msg": "Unsupported price oracle" + }, + { + "code": 6002, + "name": "InvalidOracleAccount", + "msg": "Invalid oracle account" + }, + { + "code": 6003, + "name": "StaleOraclePrice", + "msg": "Stale oracle price" + }, + { + "code": 6004, + "name": "InvalidOraclePrice", + "msg": "Invalid oracle price" + }, + { + "code": 6005, + "name": "InvalidEnvironment", + "msg": "Instruction is not allowed in production" + }, + { + "code": 6006, + "name": "InvalidCollateralAccount", + "msg": "Invalid collateral account" + }, + { + "code": 6007, + "name": "InvalidCollateralAmount", + "msg": "Invalid collateral amount" + }, + { + "code": 6008, + "name": "CollateralSlippage", + "msg": "Collateral slippage" + }, + { + "code": 6009, + "name": "InvalidPositionState", + "msg": "Invalid position state" + }, + { + "code": 6010, + "name": "InvalidPerpetualsConfig", + "msg": "Invalid perpetuals config" + }, + { + "code": 6011, + "name": "InvalidPoolConfig", + "msg": "Invalid pool config" + }, + { + "code": 6012, + "name": "InvalidInstruction", + "msg": "Invalid instruction" + }, + { + "code": 6013, + "name": "InvalidCustodyConfig", + "msg": "Invalid custody config" + }, + { + "code": 6014, + "name": "InvalidCustodyBalance", + "msg": "Invalid custody balance" + }, + { + "code": 6015, + "name": "InvalidArgument", + "msg": "Invalid argument" + }, + { + "code": 6016, + "name": "InvalidPositionRequest", + "msg": "Invalid position request" + }, + { + "code": 6017, + "name": "InvalidPositionRequestInputAta", + "msg": "Invalid position request input ata" + }, + { + "code": 6018, + "name": "InvalidMint", + "msg": "Invalid mint" + }, + { + "code": 6019, + "name": "InsufficientTokenAmount", + "msg": "Insufficient token amount" + }, + { + "code": 6020, + "name": "InsufficientAmountReturned", + "msg": "Insufficient token amount returned" + }, + { + "code": 6021, + "name": "MaxPriceSlippage", + "msg": "Price slippage limit exceeded" + }, + { + "code": 6022, + "name": "MaxLeverage", + "msg": "Position leverage limit exceeded" + }, + { + "code": 6023, + "name": "CustodyAmountLimit", + "msg": "Custody amount limit exceeded" + }, + { + "code": 6024, + "name": "PoolAmountLimit", + "msg": "Pool amount limit exceeded" + }, + { + "code": 6025, + "name": "PersonalPoolAmountLimit", + "msg": "Personal pool amount limit exceeded" + }, + { + "code": 6026, + "name": "UnsupportedToken", + "msg": "Token is not supported" + }, + { + "code": 6027, + "name": "InstructionNotAllowed", + "msg": "Instruction is not allowed at this time" + }, + { + "code": 6028, + "name": "JupiterProgramMismatch", + "msg": "Jupiter Program ID mismatch" + }, + { + "code": 6029, + "name": "ProgramMismatch", + "msg": "Program ID mismatch" + }, + { + "code": 6030, + "name": "AddressMismatch", + "msg": "Address mismatch" + }, + { + "code": 6031, + "name": "KeeperATAMissing", + "msg": "Missing keeper ATA" + }, + { + "code": 6032, + "name": "SwapAmountMismatch", + "msg": "Swap amount mismatch" + }, + { + "code": 6033, + "name": "CPINotAllowed", + "msg": "CPI not allowed" + }, + { + "code": 6034, + "name": "InvalidKeeper", + "msg": "Invalid Keeper" + }, + { + "code": 6035, + "name": "ExceedExecutionPeriod", + "msg": "Exceed execution period" + }, + { + "code": 6036, + "name": "InvalidRequestType", + "msg": "Invalid Request Type" + }, + { + "code": 6037, + "name": "InvalidTriggerPrice", + "msg": "Invalid Trigger Price" + }, + { + "code": 6038, + "name": "TriggerPriceSlippage", + "msg": "Trigger Price Slippage" + }, + { + "code": 6039, + "name": "MissingTriggerPrice", + "msg": "Missing Trigger Price" + }, + { + "code": 6040, + "name": "MissingPriceSlippage", + "msg": "Missing Price Slippage" + }, + { + "code": 6041, + "name": "InvalidPriceCalcMode", + "msg": "Invalid Price Calc Mode" + }, + { + "code": 6042, + "name": "RequestUpdatedTooRecent", + "msg": "Request Updated Too Recent" + }, + { + "code": 6043, + "name": "ExceedTokenWeightage", + "msg": "Exceed Token Weightage" + }, + { + "code": 6044, + "name": "OraclePublishTimeTooEarly", + "msg": "Oracle Publish Time Too Early" + }, + { + "code": 6045, + "name": "PullOraclePublishTimeTooEarly", + "msg": "Pull Oracle Publish Time Too Early" + }, + { + "code": 6046, + "name": "StalePullOraclePrice", + "msg": "Stale Pull Oracle Price" + }, + { + "code": 6047, + "name": "InvalidPullOraclePrice", + "msg": "Invalid Pull Oracle Price" + }, + { + "code": 6048, + "name": "PullOracleNotVerified", + "msg": "Pull Oracle Not Verified" + }, + { + "code": 6049, + "name": "PriceDiffTooLarge", + "msg": "Price Diff Between Pull and Push Oracle is Too Large" + }, + { + "code": 6050, + "name": "InvalidDovesOraclePrice", + "msg": "Invalid Doves Oracle Price" + }, + { + "code": 6051, + "name": "InvalidRequestTime", + "msg": "Invalid Request Time" + }, + { + "code": 6052, + "name": "PositionUpdatedTooRecent", + "msg": "Position Updated Too Recent" + }, + { + "code": 6053, + "name": "LedgerTokenAccountDoesNotMatch", + "msg": "Ledger token account does not match" + }, + { + "code": 6054, + "name": "InvalidTokenLedger", + "msg": "Invalid token ledger" + }, + { + "code": 6055, + "name": "OraclePriceDifferenceTooLarge", + "msg": "Oracle Price Difference Too Large" + }, + { + "code": 6056, + "name": "InvalidOracleSigner", + "msg": "Invalid Oracle Signer" + }, + { + "code": 6057, + "name": "InvalidOracleTimestamp", + "msg": "Invalid Oracle Timestamp" + }, + { + "code": 6058, + "name": "InvalidMaxGlobalLongSize", + "msg": "New Max Global Long Size Too Low" + }, + { + "code": 6059, + "name": "InvalidMaxGlobalShortSize", + "msg": "New Max Global Short Size Too Low" + }, + { + "code": 6060, + "name": "BorrowsDisabled", + "msg": "Borrows disabled for this custody" + }, + { + "code": 6061, + "name": "BorrowLimitsExceeded", + "msg": "Borrows limit exceeded for this custody" + }, + { + "code": 6062, + "name": "WithdrawExceedsMarginLimits", + "msg": "Withdraw exceeds margin of the custody" + }, + { + "code": 6063, + "name": "CannotLiquidate", + "msg": "Cannot liquidate" + }, + { + "code": 6064, + "name": "CannotDelegateStake", + "msg": "Cannot delegate stake to deactivated account" + }, + { + "code": 6065, + "name": "ExceededMaxTotalStakedAmount", + "msg": "Max total staked amount exceeded" + }, + { + "code": 6066, + "name": "LpTokenPriceChangeLimitExceeded", + "msg": "LP token price change exceeds limit" + } + ], + "types": [ + { + "name": "AddCustodyParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "is_stable", + "type": "bool" + }, + { + "name": "oracle", + "type": { + "defined": { + "name": "OracleParams" + } + } + }, + { + "name": "pricing", + "type": { + "defined": { + "name": "PricingParams" + } + } + }, + { + "name": "permissions", + "type": { + "defined": { + "name": "Permissions" + } + } + }, + { + "name": "hourly_funding_dbps", + "type": "u64" + }, + { + "name": "target_ratio_bps", + "type": "u64" + }, + { + "name": "increase_position_bps", + "type": "u64" + }, + { + "name": "decrease_position_bps", + "type": "u64" + }, + { + "name": "doves_oracle", + "type": "pubkey" + }, + { + "name": "max_position_size_usd", + "type": "u64" + }, + { + "name": "jump_rate", + "type": { + "defined": { + "name": "JumpRateState" + } + } + }, + { + "name": "price_impact_fee_factor", + "type": "u64" + }, + { + "name": "price_impact_exponent", + "type": "f32" + }, + { + "name": "delta_imbalance_threshold_decimal", + "type": "u64" + }, + { + "name": "max_fee_bps", + "type": "u64" + }, + { + "name": "doves_ag_oracle", + "type": "pubkey" + } + ] + } + }, + { + "name": "AddLiquidity2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "token_amount_in", + "type": "u64" + }, + { + "name": "min_lp_amount_out", + "type": "u64" + }, + { + "name": "token_amount_pre_swap", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "AddPoolParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "limit", + "type": { + "defined": { + "name": "Limit" + } + } + }, + { + "name": "fees", + "type": { + "defined": { + "name": "Fees" + } + } + }, + { + "name": "max_request_execution_sec", + "type": "i64" + } + ] + } + }, + { + "name": "BorrowFromCustodyParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "CloseBorrowPositionParams", + "type": { + "kind": "struct" + } + }, + { + "name": "CreateAndDelegateStakeAccountParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "stake_account_index", + "type": "u64" + }, + { + "name": "stake_amount_lamports", + "type": "u64" + } + ] + } + }, + { + "name": "CreateDecreasePositionMarketRequestParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "jupiter_minimum_out", + "type": { + "option": "u64" + } + }, + { + "name": "entire_position", + "type": { + "option": "bool" + } + }, + { + "name": "counter", + "type": "u64" + } + ] + } + }, + { + "name": "CreateDecreasePositionRequest2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "request_type", + "type": { + "defined": { + "name": "RequestType" + } + } + }, + { + "name": "price_slippage", + "type": { + "option": "u64" + } + }, + { + "name": "jupiter_minimum_out", + "type": { + "option": "u64" + } + }, + { + "name": "trigger_price", + "type": { + "option": "u64" + } + }, + { + "name": "trigger_above_threshold", + "type": { + "option": "bool" + } + }, + { + "name": "entire_position", + "type": { + "option": "bool" + } + }, + { + "name": "counter", + "type": "u64" + } + ] + } + }, + { + "name": "CreateIncreasePositionMarketRequestParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_token_delta", + "type": "u64" + }, + { + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "jupiter_minimum_out", + "type": { + "option": "u64" + } + }, + { + "name": "counter", + "type": "u64" + } + ] + } + }, + { + "name": "CreateTokenMetadataParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + } + ] + } + }, + { + "name": "DecreasePosition4Params", + "type": { + "kind": "struct" + } + }, + { + "name": "DecreasePositionWithInternalSwapParams", + "type": { + "kind": "struct" + } + }, + { + "name": "DecreasePositionWithTpslAndInternalSwapParams", + "type": { + "kind": "struct" + } + }, + { + "name": "DecreasePositionWithTpslParams", + "type": { + "kind": "struct" + } + }, + { + "name": "DepositParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "GetAddLiquidityAmountAndFee2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "token_amount_in", + "type": "u64" + } + ] + } + }, + { + "name": "GetAssetsUnderManagement2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "mode", + "type": { + "option": { + "defined": { + "name": "PriceCalcMode" + } + } + } + } + ] + } + }, + { + "name": "GetRemoveLiquidityAmountAndFee2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lp_amount_in", + "type": "u64" + } + ] + } + }, + { + "name": "IncreasePosition4Params", + "type": { + "kind": "struct" + } + }, + { + "name": "IncreasePositionPreSwapParams", + "type": { + "kind": "struct" + } + }, + { + "name": "IncreasePositionWithInternalSwapParams", + "type": { + "kind": "struct" + } + }, + { + "name": "InitParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "allow_swap", + "type": "bool" + }, + { + "name": "allow_add_liquidity", + "type": "bool" + }, + { + "name": "allow_remove_liquidity", + "type": "bool" + }, + { + "name": "allow_increase_position", + "type": "bool" + }, + { + "name": "allow_decrease_position", + "type": "bool" + }, + { + "name": "allow_collateral_withdrawal", + "type": "bool" + }, + { + "name": "allow_liquidate_position", + "type": "bool" + } + ] + } + }, + { + "name": "InstantCreateLimitOrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_token_delta", + "type": "u64" + }, + { + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "trigger_price", + "type": "u64" + }, + { + "name": "trigger_above_threshold", + "type": "bool" + }, + { + "name": "counter", + "type": "u64" + }, + { + "name": "request_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantCreateTpslParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "trigger_price", + "type": "u64" + }, + { + "name": "trigger_above_threshold", + "type": "bool" + }, + { + "name": "entire_position", + "type": "bool" + }, + { + "name": "counter", + "type": "u64" + }, + { + "name": "request_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantDecreasePosition2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "entire_position", + "type": { + "option": "bool" + } + }, + { + "name": "request_time", + "type": "i64" + }, + { + "name": "counter", + "type": "u64" + } + ] + } + }, + { + "name": "InstantDecreasePositionParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "entire_position", + "type": { + "option": "bool" + } + }, + { + "name": "request_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantIncreasePositionPreSwapParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "min_amount_out", + "type": "u64" + } + ] + } + }, + { + "name": "InstantIncreasePositionParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_token_delta", + "type": { + "option": "u64" + } + }, + { + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "request_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantUpdateLimitOrderParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "trigger_price", + "type": "u64" + }, + { + "name": "request_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantUpdateTpslParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "trigger_price", + "type": "u64" + }, + { + "name": "request_time", + "type": "i64" + } + ] + } + }, + { + "name": "LiquidateBorrowPositionParams", + "type": { + "kind": "struct" + } + }, + { + "name": "LiquidateFullPosition4Params", + "type": { + "kind": "struct" + } + }, + { + "name": "OperatorSetCustodyConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pricing", + "type": { + "defined": { + "name": "PricingParams" + } + } + }, + { + "name": "hourly_funding_dbps", + "type": "u64" + }, + { + "name": "target_ratio_bps", + "type": "u64" + }, + { + "name": "increase_position_bps", + "type": "u64" + }, + { + "name": "decrease_position_bps", + "type": "u64" + }, + { + "name": "max_position_size_usd", + "type": "u64" + }, + { + "name": "jump_rate", + "type": { + "defined": { + "name": "JumpRateState" + } + } + }, + { + "name": "price_impact_fee_factor", + "type": "u64" + }, + { + "name": "price_impact_exponent", + "type": "f32" + }, + { + "name": "delta_imbalance_threshold_decimal", + "type": "u64" + }, + { + "name": "max_fee_bps", + "type": "u64" + }, + { + "name": "borrow_lend_parameters", + "type": { + "defined": { + "name": "BorrowLendParams" + } + } + }, + { + "name": "borrow_hourly_funding_dbps", + "type": "u64" + }, + { + "name": "borrow_limit_in_token_amount", + "type": "u64" + }, + { + "name": "min_interest_fee_bps", + "type": "u64" + }, + { + "name": "min_interest_fee_grace_period_seconds", + "type": "u64" + }, + { + "name": "max_total_staked_amount_lamports", + "type": "u64" + }, + { + "name": "external_swap_fee_multiplier_bps", + "type": "u64" + }, + { + "name": "disable_close_position_request", + "type": "bool" + }, + { + "name": "withdrawal_limit_token_amount", + "type": "u64" + }, + { + "name": "withdrawal_limit_interval_seconds", + "type": "u64" + } + ] + } + }, + { + "name": "OperatorSetPoolConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "fees", + "type": { + "defined": { + "name": "Fees" + } + } + }, + { + "name": "limit", + "type": { + "defined": { + "name": "Limit" + } + } + }, + { + "name": "max_request_execution_sec", + "type": "i64" + }, + { + "name": "max_trigger_price_diff_bps", + "type": "u64" + }, + { + "name": "disable_close_position_request", + "type": "bool" + }, + { + "name": "max_lp_token_price_change_bps", + "type": "u64" + } + ] + } + }, + { + "name": "PartialLiquidateBorrowPositionParams", + "type": { + "kind": "struct" + } + }, + { + "name": "RefreshAssetsUnderManagementParams", + "type": { + "kind": "struct" + } + }, + { + "name": "RemoveLiquidity2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lp_amount_in", + "type": "u64" + }, + { + "name": "min_amount_out", + "type": "u64" + } + ] + } + }, + { + "name": "RepayToCustodyParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "SetCustodyConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle", + "type": { + "defined": { + "name": "OracleParams" + } + } + }, + { + "name": "pricing", + "type": { + "defined": { + "name": "PricingParams" + } + } + }, + { + "name": "permissions", + "type": { + "defined": { + "name": "Permissions" + } + } + }, + { + "name": "hourly_funding_dbps", + "type": "u64" + }, + { + "name": "target_ratio_bps", + "type": "u64" + }, + { + "name": "increase_position_bps", + "type": "u64" + }, + { + "name": "decrease_position_bps", + "type": "u64" + }, + { + "name": "doves_oracle", + "type": "pubkey" + }, + { + "name": "max_position_size_usd", + "type": "u64" + }, + { + "name": "jump_rate", + "type": { + "defined": { + "name": "JumpRateState" + } + } + }, + { + "name": "price_impact_fee_factor", + "type": "u64" + }, + { + "name": "price_impact_exponent", + "type": "f32" + }, + { + "name": "delta_imbalance_threshold_decimal", + "type": "u64" + }, + { + "name": "max_fee_bps", + "type": "u64" + }, + { + "name": "doves_ag_oracle", + "type": "pubkey" + }, + { + "name": "borrow_lend_parameters", + "type": { + "defined": { + "name": "BorrowLendParams" + } + } + }, + { + "name": "borrow_hourly_funding_dbps", + "type": "u64" + }, + { + "name": "borrow_limit_in_token_amount", + "type": "u64" + }, + { + "name": "min_interest_fee_bps", + "type": "u64" + }, + { + "name": "min_interest_fee_grace_period_seconds", + "type": "u64" + }, + { + "name": "max_total_staked_amount_lamports", + "type": "u64" + }, + { + "name": "external_swap_fee_multiplier_bps", + "type": "u64" + }, + { + "name": "disable_close_position_request", + "type": "bool" + }, + { + "name": "withdrawal_limit_interval_seconds", + "type": "u64" + }, + { + "name": "withdrawal_limit_token_amount", + "type": "u64" + } + ] + } + }, + { + "name": "SetMaxGlobalSizesParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_global_long_size", + "type": "u64" + }, + { + "name": "max_global_short_size", + "type": "u64" + }, + { + "name": "recovery_id", + "type": "u8" + }, + { + "name": "signature", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "reference_id", + "type": { + "array": [ + "u8", + 16 + ] + } + }, + { + "name": "timestamp", + "type": "u64" + } + ] + } + }, + { + "name": "SetPerpetualsConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "permissions", + "type": { + "defined": { + "name": "Permissions" + } + } + } + ] + } + }, + { + "name": "SetPoolConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "fees", + "type": { + "defined": { + "name": "Fees" + } + } + }, + { + "name": "limit", + "type": { + "defined": { + "name": "Limit" + } + } + }, + { + "name": "max_request_execution_sec", + "type": "i64" + }, + { + "name": "parameter_update_oracle", + "type": { + "defined": { + "name": "Secp256k1Pubkey" + } + } + }, + { + "name": "max_trigger_price_diff_bps", + "type": "u64" + }, + { + "name": "disable_close_position_request", + "type": "bool" + }, + { + "name": "max_lp_token_price_change_bps", + "type": "u64" + } + ] + } + }, + { + "name": "SetTestTimeParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "time", + "type": "i64" + } + ] + } + }, + { + "name": "Swap2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "min_amount_out", + "type": "u64" + } + ] + } + }, + { + "name": "SwapWithTokenLedgerParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "min_amount_out", + "type": "u64" + } + ] + } + }, + { + "name": "TestInitParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "allow_swap", + "type": "bool" + }, + { + "name": "allow_add_liquidity", + "type": "bool" + }, + { + "name": "allow_remove_liquidity", + "type": "bool" + }, + { + "name": "allow_increase_position", + "type": "bool" + }, + { + "name": "allow_decrease_position", + "type": "bool" + }, + { + "name": "allow_collateral_withdrawal", + "type": "bool" + }, + { + "name": "allow_liquidate_position", + "type": "bool" + } + ] + } + }, + { + "name": "TransferAdminParams", + "type": { + "kind": "struct" + } + }, + { + "name": "UpdateDecreasePositionRequest2Params", + "type": { + "kind": "struct", + "fields": [ + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "trigger_price", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawFees2Params", + "type": { + "kind": "struct" + } + }, + { + "name": "PriceImpactBuffer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "open_interest", + "type": { + "array": [ + "i64", + 60 + ] + } + }, + { + "name": "last_updated", + "type": "i64" + }, + { + "name": "fee_factor", + "type": "u64" + }, + { + "name": "exponent", + "type": "f32" + }, + { + "name": "delta_imbalance_threshold_decimal", + "type": "u64" + }, + { + "name": "max_fee_bps", + "type": "u64" + } + ] + } + }, + { + "name": "Assets", + "type": { + "kind": "struct", + "fields": [ + { + "name": "fees_reserves", + "type": "u64" + }, + { + "name": "owned", + "type": "u64" + }, + { + "name": "locked", + "type": "u64" + }, + { + "name": "guaranteed_usd", + "type": "u64" + }, + { + "name": "global_short_sizes", + "type": "u64" + }, + { + "name": "global_short_average_prices", + "type": "u64" + } + ] + } + }, + { + "name": "PricingParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trade_impact_fee_scalar", + "type": "u64" + }, + { + "name": "buffer", + "type": "u64" + }, + { + "name": "swap_spread", + "type": "u64" + }, + { + "name": "max_leverage", + "type": "u64" + }, + { + "name": "max_global_long_sizes", + "type": "u64" + }, + { + "name": "max_global_short_sizes", + "type": "u64" + } + ] + } + }, + { + "name": "FundingRateState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "cumulative_interest_rate", + "type": "u128" + }, + { + "name": "last_update", + "type": "i64" + }, + { + "name": "hourly_funding_dbps", + "type": "u64" + } + ] + } + }, + { + "name": "JumpRateState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "min_rate_bps", + "type": "u64" + }, + { + "name": "max_rate_bps", + "type": "u64" + }, + { + "name": "target_rate_bps", + "type": "u64" + }, + { + "name": "target_utilization_rate", + "type": "u64" + } + ] + } + }, + { + "name": "BorrowLendParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "borrows_limit_in_bps", + "type": "u64" + }, + { + "name": "maintainance_margin_bps", + "type": "u64" + }, + { + "name": "protocol_fee_bps", + "type": "u64" + }, + { + "name": "liquidation_margin", + "type": "u64" + }, + { + "name": "liquidation_fee_bps", + "type": "u64" + } + ] + } + }, + { + "name": "OraclePrice", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "exponent", + "type": "i32" + } + ] + } + }, + { + "name": "Price", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "u64" + }, + { + "name": "expo", + "type": "i32" + }, + { + "name": "publish_time", + "type": "i64" + } + ] + } + }, + { + "name": "OracleParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle_account", + "type": "pubkey" + }, + { + "name": "oracle_type", + "type": { + "defined": { + "name": "OracleType" + } + } + }, + { + "name": "buffer", + "type": "u64" + }, + { + "name": "max_price_age_sec", + "type": "u32" + } + ] + } + }, + { + "name": "AmountAndFee", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "fee_bps", + "type": "u64" + } + ] + } + }, + { + "name": "Permissions", + "type": { + "kind": "struct", + "fields": [ + { + "name": "allow_swap", + "type": "bool" + }, + { + "name": "allow_add_liquidity", + "type": "bool" + }, + { + "name": "allow_remove_liquidity", + "type": "bool" + }, + { + "name": "allow_increase_position", + "type": "bool" + }, + { + "name": "allow_decrease_position", + "type": "bool" + }, + { + "name": "allow_collateral_withdrawal", + "type": "bool" + }, + { + "name": "allow_liquidate_position", + "type": "bool" + } + ] + } + }, + { + "name": "Fees", + "type": { + "kind": "struct", + "fields": [ + { + "name": "swap_multiplier", + "type": "u64" + }, + { + "name": "stable_swap_multiplier", + "type": "u64" + }, + { + "name": "add_remove_liquidity_bps", + "type": "u64" + }, + { + "name": "swap_bps", + "type": "u64" + }, + { + "name": "tax_bps", + "type": "u64" + }, + { + "name": "stable_swap_bps", + "type": "u64" + }, + { + "name": "stable_swap_tax_bps", + "type": "u64" + }, + { + "name": "liquidation_reward_bps", + "type": "u64" + }, + { + "name": "protocol_share_bps", + "type": "u64" + } + ] + } + }, + { + "name": "PoolApr", + "type": { + "kind": "struct", + "fields": [ + { + "name": "last_updated", + "type": "i64" + }, + { + "name": "fee_apr_bps", + "type": "u64" + }, + { + "name": "realized_fee_usd", + "type": "u64" + } + ] + } + }, + { + "name": "Limit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_aum_usd", + "type": "u128" + }, + { + "name": "token_weightage_buffer_bps", + "type": "u128" + }, + { + "name": "buffer", + "type": "u64" + } + ] + } + }, + { + "name": "Secp256k1Pubkey", + "type": { + "kind": "struct", + "fields": [ + { + "name": "prefix", + "type": "u8" + }, + { + "name": "key", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "PriceImpactMechanism", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TradeSize" + }, + { + "name": "DeltaImbalance" + } + ] + } + }, + { + "name": "OracleType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Test" + }, + { + "name": "Pyth" + } + ] + } + }, + { + "name": "PriceCalcMode", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Min" + }, + { + "name": "Max" + }, + { + "name": "Ignore" + } + ] + } + }, + { + "name": "PriceStaleTolerance", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Strict" + }, + { + "name": "Loose" + } + ] + } + }, + { + "name": "TradePoolType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Increase" + }, + { + "name": "Decrease" + } + ] + } + }, + { + "name": "RequestType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Market" + }, + { + "name": "Trigger" + } + ] + } + }, + { + "name": "RequestChange", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Increase" + }, + { + "name": "Decrease" + } + ] + } + }, + { + "name": "Side", + "type": { + "kind": "enum", + "variants": [ + { + "name": "None" + }, + { + "name": "Long" + }, + { + "name": "Short" + } + ] + } + }, + { + "name": "BorrowPosition", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "update_time", + "type": "i64" + }, + { + "name": "borrow_size", + "type": "u128" + }, + { + "name": "cumulative_compounded_interest_snapshot", + "type": "u128" + }, + { + "name": "locked_collateral", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "last_borrowed", + "type": "i64" + } + ] + } + }, + { + "name": "Custody", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "token_account", + "type": "pubkey" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "is_stable", + "type": "bool" + }, + { + "name": "oracle", + "type": { + "defined": { + "name": "OracleParams" + } + } + }, + { + "name": "pricing", + "type": { + "defined": { + "name": "PricingParams" + } + } + }, + { + "name": "permissions", + "type": { + "defined": { + "name": "Permissions" + } + } + }, + { + "name": "target_ratio_bps", + "type": "u64" + }, + { + "name": "assets", + "type": { + "defined": { + "name": "Assets" + } + } + }, + { + "name": "funding_rate_state", + "type": { + "defined": { + "name": "FundingRateState" + } + } + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "token_account_bump", + "type": "u8" + }, + { + "name": "increase_position_bps", + "type": "u64" + }, + { + "name": "decrease_position_bps", + "type": "u64" + }, + { + "name": "max_position_size_usd", + "type": "u64" + }, + { + "name": "doves_oracle", + "type": "pubkey" + }, + { + "name": "jump_rate_state", + "type": { + "defined": { + "name": "JumpRateState" + } + } + }, + { + "name": "doves_ag_oracle", + "type": "pubkey" + }, + { + "name": "price_impact_buffer", + "type": { + "defined": { + "name": "PriceImpactBuffer" + } + } + }, + { + "name": "borrow_lend_parameters", + "type": { + "defined": { + "name": "BorrowLendParams" + } + } + }, + { + "name": "borrows_funding_rate_state", + "type": { + "defined": { + "name": "FundingRateState" + } + } + }, + { + "name": "debt", + "type": "u128" + }, + { + "name": "borrow_lend_interests_accured", + "type": "u128" + }, + { + "name": "borrow_limit_in_token_amount", + "type": "u64" + }, + { + "name": "min_interest_fee_bps", + "type": "u64" + }, + { + "name": "min_interest_fee_grace_period_seconds", + "type": "u64" + }, + { + "name": "total_staked_amount_lamports", + "type": "u64" + }, + { + "name": "max_total_staked_amount_lamports", + "type": "u64" + }, + { + "name": "external_swap_fee_multiplier_bps", + "type": "u64" + }, + { + "name": "disable_close_position_request", + "type": "bool" + }, + { + "name": "withdrawal_limit_token_amount", + "type": "u64" + }, + { + "name": "withdrawal_token_amount_accumulated", + "type": "u64" + }, + { + "name": "withdrawal_limit_last_reset_at", + "type": "i64" + }, + { + "name": "withdrawal_limit_interval_seconds", + "type": "u64" + } + ] + } + }, + { + "name": "Perpetuals", + "type": { + "kind": "struct", + "fields": [ + { + "name": "permissions", + "type": { + "defined": { + "name": "Permissions" + } + } + }, + { + "name": "pools", + "type": { + "vec": "pubkey" + } + }, + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "transfer_authority_bump", + "type": "u8" + }, + { + "name": "perpetuals_bump", + "type": "u8" + }, + { + "name": "inception_time", + "type": "i64" + } + ] + } + }, + { + "name": "Pool", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "custodies", + "type": { + "vec": "pubkey" + } + }, + { + "name": "aum_usd", + "type": "u128" + }, + { + "name": "limit", + "type": { + "defined": { + "name": "Limit" + } + } + }, + { + "name": "fees", + "type": { + "defined": { + "name": "Fees" + } + } + }, + { + "name": "pool_apr", + "type": { + "defined": { + "name": "PoolApr" + } + } + }, + { + "name": "max_request_execution_sec", + "type": "i64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "lp_token_bump", + "type": "u8" + }, + { + "name": "inception_time", + "type": "i64" + }, + { + "name": "parameter_update_oracle", + "type": { + "defined": { + "name": "Secp256k1Pubkey" + } + } + }, + { + "name": "aum_usd_updated_at", + "type": "i64" + }, + { + "name": "max_trigger_price_diff_bps", + "type": "u64" + }, + { + "name": "disable_close_position_request", + "type": "bool" + }, + { + "name": "max_lp_token_price_change_bps", + "type": "u64" + }, + { + "name": "aum_usd_refreshed_at_slot", + "type": "u64" + } + ] + } + }, + { + "name": "PositionRequest", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "position", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "update_time", + "type": "i64" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_delta", + "type": "u64" + }, + { + "name": "request_change", + "type": { + "defined": { + "name": "RequestChange" + } + } + }, + { + "name": "request_type", + "type": { + "defined": { + "name": "RequestType" + } + } + }, + { + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "price_slippage", + "type": { + "option": "u64" + } + }, + { + "name": "jupiter_minimum_out", + "type": { + "option": "u64" + } + }, + { + "name": "pre_swap_amount", + "type": { + "option": "u64" + } + }, + { + "name": "trigger_price", + "type": { + "option": "u64" + } + }, + { + "name": "trigger_above_threshold", + "type": { + "option": "bool" + } + }, + { + "name": "entire_position", + "type": { + "option": "bool" + } + }, + { + "name": "executed", + "type": "bool" + }, + { + "name": "counter", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "referral", + "type": { + "option": "pubkey" + } + } + ] + } + }, + { + "name": "Position", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "collateral_custody", + "type": "pubkey" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "update_time", + "type": "i64" + }, + { + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "size_usd", + "type": "u64" + }, + { + "name": "collateral_usd", + "type": "u64" + }, + { + "name": "realised_pnl_usd", + "type": "i64" + }, + { + "name": "cumulative_interest_snapshot", + "type": "u128" + }, + { + "name": "locked_amount", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "StakeInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "stake_account", + "type": "pubkey" + }, + { + "name": "current_staked_amount_lamports", + "type": "u64" + }, + { + "name": "total_staking_rewards_lamports", + "type": "u64" + }, + { + "name": "last_updated_at", + "type": "i64" + }, + { + "name": "deactivating", + "type": "bool" + }, + { + "name": "stake_account_index", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "TokenLedger", + "type": { + "kind": "struct", + "fields": [ + { + "name": "token_account", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "CreatePositionRequestEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_mint", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "position_request_mint", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_delta", + "type": "u64" + }, + { + "name": "price_slippage", + "type": { + "option": "u64" + } + }, + { + "name": "jupiter_minimum_out", + "type": { + "option": "u64" + } + }, + { + "name": "pre_swap_amount", + "type": { + "option": "u64" + } + }, + { + "name": "request_change", + "type": "u8" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "referral", + "type": { + "option": "pubkey" + } + } + ] + } + }, + { + "name": "InstantCreateTpslEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "position_request_mint", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_delta", + "type": "u64" + }, + { + "name": "entire_position", + "type": "bool" + }, + { + "name": "open_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantUpdateTpslEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "position_request_mint", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_delta", + "type": "u64" + }, + { + "name": "entire_position", + "type": "bool" + }, + { + "name": "update_time", + "type": "i64" + } + ] + } + }, + { + "name": "ClosePositionRequestEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "entire_position", + "type": { + "option": "bool" + } + }, + { + "name": "executed", + "type": "bool" + }, + { + "name": "request_change", + "type": "u8" + }, + { + "name": "request_type", + "type": "u8" + }, + { + "name": "side", + "type": "u8" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + } + ] + } + }, + { + "name": "IncreasePositionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_size_usd", + "type": "u64" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "position_request_mint", + "type": "pubkey" + }, + { + "name": "position_request_change", + "type": "u8" + }, + { + "name": "position_request_type", + "type": "u8" + }, + { + "name": "position_request_collateral_delta", + "type": "u64" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "collateral_token_delta", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "price_slippage", + "type": { + "option": "u64" + } + }, + { + "name": "fee_token", + "type": "u64" + }, + { + "name": "fee_usd", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "referral", + "type": { + "option": "pubkey" + } + }, + { + "name": "position_fee_usd", + "type": "u64" + }, + { + "name": "funding_fee_usd", + "type": "u64" + }, + { + "name": "price_impact_fee_usd", + "type": "u64" + } + ] + } + }, + { + "name": "IncreasePositionPreSwapEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "transfer_amount", + "type": "u64" + }, + { + "name": "collateral_custody_pre_swap_amount", + "type": "u64" + } + ] + } + }, + { + "name": "DecreasePositionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_size_usd", + "type": "u64" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "position_request_mint", + "type": "pubkey" + }, + { + "name": "position_request_change", + "type": "u8" + }, + { + "name": "position_request_type", + "type": "u8" + }, + { + "name": "has_profit", + "type": "bool" + }, + { + "name": "pnl_delta", + "type": "u64" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "transfer_amount_usd", + "type": "u64" + }, + { + "name": "transfer_token", + "type": { + "option": "u64" + } + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "price_slippage", + "type": { + "option": "u64" + } + }, + { + "name": "fee_usd", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "referral", + "type": { + "option": "pubkey" + } + }, + { + "name": "position_fee_usd", + "type": "u64" + }, + { + "name": "funding_fee_usd", + "type": "u64" + }, + { + "name": "price_impact_fee_usd", + "type": "u64" + }, + { + "name": "original_position_collateral_usd", + "type": "u64" + }, + { + "name": "position_collateral_usd", + "type": "u64" + }, + { + "name": "position_open_time", + "type": "i64" + }, + { + "name": "position_price", + "type": "u64" + } + ] + } + }, + { + "name": "DecreasePositionPostSwapEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "swap_amount", + "type": "u64" + }, + { + "name": "jupiter_minimum_out", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "LiquidateFullPositionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_mint", + "type": "pubkey" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_size_usd", + "type": "u64" + }, + { + "name": "has_profit", + "type": "bool" + }, + { + "name": "pnl_delta", + "type": "u64" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "transfer_amount_usd", + "type": "u64" + }, + { + "name": "transfer_token", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "fee_usd", + "type": "u64" + }, + { + "name": "liquidation_fee_usd", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "position_fee_usd", + "type": "u64" + }, + { + "name": "funding_fee_usd", + "type": "u64" + }, + { + "name": "price_impact_fee_usd", + "type": "u64" + }, + { + "name": "original_position_collateral_usd", + "type": "u64" + }, + { + "name": "position_collateral_usd", + "type": "u64" + }, + { + "name": "position_open_time", + "type": "i64" + }, + { + "name": "position_price", + "type": "u64" + } + ] + } + }, + { + "name": "PoolSwapEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiving_custody_key", + "type": "pubkey" + }, + { + "name": "dispensing_custody_key", + "type": "pubkey" + }, + { + "name": "pool_key", + "type": "pubkey" + }, + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "swap_usd_amount", + "type": "u64" + }, + { + "name": "amount_out_after_fees", + "type": "u64" + }, + { + "name": "fee_bps", + "type": "u64" + }, + { + "name": "owner_key", + "type": "pubkey" + }, + { + "name": "receiving_account_key", + "type": "pubkey" + } + ] + } + }, + { + "name": "PoolSwapExactOutEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiving_custody_key", + "type": "pubkey" + }, + { + "name": "dispensing_custody_key", + "type": "pubkey" + }, + { + "name": "pool_key", + "type": "pubkey" + }, + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "amount_in_after_fees", + "type": "u64" + }, + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "swap_usd_amount", + "type": "u64" + }, + { + "name": "fee_bps", + "type": "u64" + }, + { + "name": "owner_key", + "type": "pubkey" + }, + { + "name": "receiving_account_key", + "type": "pubkey" + } + ] + } + }, + { + "name": "AddLiquidityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "custody_key", + "type": "pubkey" + }, + { + "name": "pool_key", + "type": "pubkey" + }, + { + "name": "token_amount_in", + "type": "u64" + }, + { + "name": "pre_pool_amount_usd", + "type": "u128" + }, + { + "name": "token_amount_usd", + "type": "u64" + }, + { + "name": "fee_bps", + "type": "u64" + }, + { + "name": "token_amount_after_fee", + "type": "u64" + }, + { + "name": "mint_amount_usd", + "type": "u64" + }, + { + "name": "lp_amount", + "type": "u64" + }, + { + "name": "post_pool_amount_usd", + "type": "u128" + } + ] + } + }, + { + "name": "RemoveLiquidityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "custody_key", + "type": "pubkey" + }, + { + "name": "pool_key", + "type": "pubkey" + }, + { + "name": "lp_amount_in", + "type": "u64" + }, + { + "name": "remove_amount_usd", + "type": "u64" + }, + { + "name": "fee_bps", + "type": "u64" + }, + { + "name": "remove_token_amount", + "type": "u64" + }, + { + "name": "token_amount_after_fee", + "type": "u64" + }, + { + "name": "post_pool_amount_usd", + "type": "u128" + } + ] + } + }, + { + "name": "InstantCreateLimitOrderEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_mint", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_request_key", + "type": "pubkey" + }, + { + "name": "position_request_mint", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_delta", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + } + ] + } + }, + { + "name": "InstantIncreasePositionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_size_usd", + "type": "u64" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "collateral_usd_delta", + "type": "u64" + }, + { + "name": "collateral_token_delta", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "fee_token", + "type": "u64" + }, + { + "name": "fee_usd", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "referral", + "type": { + "option": "pubkey" + } + }, + { + "name": "position_fee_usd", + "type": "u64" + }, + { + "name": "funding_fee_usd", + "type": "u64" + }, + { + "name": "price_impact_fee_usd", + "type": "u64" + } + ] + } + }, + { + "name": "InstantDecreasePositionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_side", + "type": "u8" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_collateral_custody", + "type": "pubkey" + }, + { + "name": "position_size_usd", + "type": "u64" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "desired_mint", + "type": "pubkey" + }, + { + "name": "has_profit", + "type": "bool" + }, + { + "name": "pnl_delta", + "type": "u64" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "size_usd_delta", + "type": "u64" + }, + { + "name": "transfer_amount_usd", + "type": "u64" + }, + { + "name": "transfer_token", + "type": "u64" + }, + { + "name": "price", + "type": "u64" + }, + { + "name": "price_slippage", + "type": "u64" + }, + { + "name": "fee_usd", + "type": "u64" + }, + { + "name": "open_time", + "type": "i64" + }, + { + "name": "referral", + "type": { + "option": "pubkey" + } + }, + { + "name": "position_fee_usd", + "type": "u64" + }, + { + "name": "funding_fee_usd", + "type": "u64" + }, + { + "name": "original_position_collateral_usd", + "type": "u64" + }, + { + "name": "position_collateral_usd", + "type": "u64" + }, + { + "name": "price_impact_fee_usd", + "type": "u64" + }, + { + "name": "position_open_time", + "type": "i64" + }, + { + "name": "position_price", + "type": "u64" + }, + { + "name": "position_request", + "type": { + "option": "pubkey" + } + } + ] + } + }, + { + "name": "DepositCollateralEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "deposit_amount", + "type": "u64" + }, + { + "name": "user_token_account", + "type": "pubkey" + }, + { + "name": "time", + "type": "i64" + } + ] + } + }, + { + "name": "WithdrawCollateralEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "withdraw_amount", + "type": "u64" + }, + { + "name": "user_token_account", + "type": "pubkey" + }, + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "previous_collateral_amount", + "type": "u64" + }, + { + "name": "collateral_amount", + "type": "u64" + }, + { + "name": "collateral_amount_usd", + "type": "u64" + }, + { + "name": "margin_usd", + "type": "u64" + }, + { + "name": "time", + "type": "i64" + } + ] + } + }, + { + "name": "BorrowFromCustodyEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "size_custody_token", + "type": "u64" + }, + { + "name": "collateral_amount", + "type": "u64" + }, + { + "name": "collateral_amount_usd", + "type": "u64" + }, + { + "name": "margin_usd", + "type": "u64" + }, + { + "name": "update_time", + "type": "i64" + } + ] + } + }, + { + "name": "RepayToCustodyEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_mint", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "size_custody_token", + "type": "u64" + }, + { + "name": "update_time", + "type": "i64" + }, + { + "name": "interest", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidateBorrowPositionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "position_key", + "type": "pubkey" + }, + { + "name": "position_custody", + "type": "pubkey" + }, + { + "name": "position_size_usd", + "type": "u64" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "collateral_locked_in_usd", + "type": "u64" + }, + { + "name": "collateral_locked_in_lp", + "type": "u64" + }, + { + "name": "remaining_collateral_in_lp", + "type": "u64" + }, + { + "name": "custody_token_price", + "type": "u64" + }, + { + "name": "total_borrows_in_usd", + "type": "u64" + }, + { + "name": "liquidation_fee_usd", + "type": "u64" + }, + { + "name": "liquidation_time", + "type": "i64" + } + ] + } + }, + { + "name": "RedeemStakeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "stake_account", + "type": "pubkey" + }, + { + "name": "stake_info", + "type": "pubkey" + }, + { + "name": "stake_rewards", + "type": "u64" + }, + { + "name": "custody_total_staked_amount", + "type": "u64" + }, + { + "name": "current_staked_amount", + "type": "u64" + }, + { + "name": "total_staking_rewards", + "type": "u64" + }, + { + "name": "redeem_time", + "type": "i64" + } + ] + } + }, + { + "name": "WithdrawStakeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "stake_account", + "type": "pubkey" + }, + { + "name": "stake_info", + "type": "pubkey" + }, + { + "name": "stake_rewards", + "type": "u64" + }, + { + "name": "custody_total_staked_amount", + "type": "u64" + }, + { + "name": "total_staking_rewards", + "type": "u64" + }, + { + "name": "withdraw_amount", + "type": "u64" + }, + { + "name": "withdraw_time", + "type": "i64" + } + ] + } + }, + { + "name": "DelegateStakeEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "stake_account", + "type": "pubkey" + }, + { + "name": "stake_info", + "type": "pubkey" + }, + { + "name": "custody_total_staked_amount", + "type": "u64" + }, + { + "name": "stake_amount", + "type": "u64" + }, + { + "name": "validator_vote_account", + "type": "pubkey" + }, + { + "name": "delegate_time", + "type": "i64" + } + ] + } + }, + { + "name": "WithdrawFeesEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "custody", + "type": "pubkey" + }, + { + "name": "custody_mint", + "type": "pubkey" + }, + { + "name": "receiving_token_account", + "type": "pubkey" + }, + { + "name": "total_trade_swap_fees", + "type": "u64" + }, + { + "name": "pool_trade_swap_fees", + "type": "u64" + }, + { + "name": "protocol_trade_swap_fees", + "type": "u64" + }, + { + "name": "total_borrow_lending_fees", + "type": "u64" + }, + { + "name": "pool_borrow_lending_fees", + "type": "u64" + }, + { + "name": "protocol_borrow_lending_fees", + "type": "u64" + }, + { + "name": "pool_total_fees_usd", + "type": "u64" + }, + { + "name": "apr_bps_before", + "type": "u64" + }, + { + "name": "apr_bps_after", + "type": "u64" + }, + { + "name": "apr_bps_updated_at", + "type": "i64" + }, + { + "name": "pool_realized_fee_usd_before", + "type": "u64" + }, + { + "name": "pool_realized_fee_usd_after", + "type": "u64" + }, + { + "name": "curtime", + "type": "i64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml new file mode 100644 index 000000000..6343ad6a8 --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml @@ -0,0 +1,239 @@ +protocol: jupiter-perps +version: v1 +account_type: Custody +idl_file_path: idl.json + +tags: + - perpetuals + - risk-engine + - liquidity-pool + - defi + +templates: + - id: jupiter-perps-custody-risk + name: Override Custody Risk Limits + description: Override a Jupiter Perps market's liquidation leverage, size caps and switches + idl_account_name: Custody + properties: + - path: pricing.max_leverage + label: Max leverage + description: "Leverage at which positions are liquidated, in BPS (10000 = 1x). Example: 5000000 is 500x" + - path: max_position_size_usd + label: Max position size + description: "Largest single position, USD with 6 decimals. Example: 10000000000000 is $10M" + - path: pricing.max_global_long_sizes + label: Max total longs + description: "Cap on all open longs, USD with 6 decimals. Example: 240000000000000 is $240M" + - path: pricing.max_global_short_sizes + label: Max total shorts + description: "Cap on all open shorts, USD with 6 decimals. Example: 112290000000000 is $112.29M" + - path: permissions.allow_swap + label: Allow swaps + description: "false refuses swaps through this custody. Example: false" + - path: permissions.allow_add_liquidity + label: Allow deposits + description: "false refuses JLP deposits in this token. Example: false" + - path: permissions.allow_remove_liquidity + label: Allow withdrawals + description: "false refuses JLP withdrawals in this token. Example: false" + - path: permissions.allow_increase_position + label: Allow opening + description: "false refuses opening or increasing positions. Example: false" + - path: permissions.allow_decrease_position + label: Allow closing + description: "false refuses closing or decreasing positions. Example: false" + - path: permissions.allow_collateral_withdrawal + label: Allow collateral withdrawal + description: "false refuses withdrawing position collateral. Example: false" + - path: permissions.allow_liquidate_position + label: Allow liquidations + description: "false stops keepers liquidating positions in this market. Example: false" + address: + type: pubkey + llm_context: | + Use this template to change a Jupiter Perps market's liquidation threshold, size caps or + action switches. The address is the custody of the traded token: SOL longs and SOL shorts + both live on the SOL custody. + + FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool + 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its + custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint + is the token's mint. Set fetchBeforeUse: true. + + pricing.max_leverage is the liquidation threshold, in BPS (10000 = 1x). A keeper + liquidates a position once collateral + PnL - close and borrow fees falls below + size x 10000 / max_leverage. Lowering it liquidates positions without moving the price: + at 200000 (20x), every position whose size is over 20 times its remaining margin can be + liquidated. + + permissions: false refuses that action with error 6027. allow_liquidate_position false + stops keepers liquidating this market. The size caps are USD with 6 decimals. + + EXAMPLE - liquidate every SOL position above 20x: + pricing.max_leverage: 200000 + + - id: jupiter-perps-custody-borrow-rate + name: Override Custody Borrow Rate + description: Override the borrow-rate curve a Jupiter Perps custody charges positions + idl_account_name: Custody + properties: + - path: jump_rate_state.min_rate_bps + label: Min rate + description: "Yearly borrow rate at 0% utilization, in BPS. Example: 1000 is 10%" + - path: jump_rate_state.max_rate_bps + label: Max rate + description: "Yearly borrow rate at 100% utilization, in BPS. Example: 15000 is 150%" + - path: jump_rate_state.target_rate_bps + label: Target rate + description: "Yearly borrow rate at the target utilization, in BPS. Example: 3500 is 35%" + - path: jump_rate_state.target_utilization_rate + label: Target utilization + description: "Utilization where the curve bends, scaled by 10^9. Example: 800000000 is 80%" + address: + type: pubkey + llm_context: | + Use this template to make borrowing expensive, so positions lose margin over time. + Jupiter Perps charges a yearly rate from a curve on the custody's utilization + (assets.locked / assets.owned): min_rate_bps at 0%, target_rate_bps at + target_utilization_rate, max_rate_bps at 100%. Set all three rates to one value for a + flat rate. + + The fee accrues into funding_rate_state.cumulative_interest_rate whenever a transaction + touches the custody, liquidations included, over the time since its last update; move + time on with surfnet_timeTravel after the override. A position owes + (cumulative_interest_rate - its cumulative_interest_snapshot) x size / 10^9 against its + margin. Leave funding_rate_state alone: transactions rewrite it. + + FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool + 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its + custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint + is the token's mint. Set fetchBeforeUse: true. A long pays its own token's custody. A + short does not pay the traded token's rate; its fee follows the USDC or USDT custody + holding its collateral. Those custodies also carry borrow-lend debt, and a high rate or a + long time jump on them overflows (error 6000). + + EXAMPLE - 1000% a year on SOL: + jump_rate_state.min_rate_bps: 100000 + jump_rate_state.target_rate_bps: 100000 + jump_rate_state.max_rate_bps: 100000 + + - id: jupiter-perps-custody-fees + name: Override Custody Trading Fees + description: Override the fees a Jupiter Perps market charges to open and close positions + idl_account_name: Custody + properties: + - path: increase_position_bps + label: Open fee + description: "Fee on the size of a position opened or increased, in BPS. Example: 6 is 0.06%" + - path: decrease_position_bps + label: Close fee + description: "Fee on the size of a position closed or liquidated, in BPS. Example: 6 is 0.06%" + - path: pricing.trade_impact_fee_scalar + label: Price impact scalar + description: "The price impact fee rate is size / this, rounded up to a whole BPS; both USD with 6 decimals. Example: 1000000000000 charges a $1,000 trade 10 BPS" + address: + type: pubkey + llm_context: | + Use this template to change what a Jupiter Perps market charges to open and close + positions. The address is the custody of the traded token: SOL longs and SOL shorts both + pay the SOL custody's fees. + + FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool + 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its + custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint + is the token's mint. Set fetchBeforeUse: true. + + Fees come out of the position's collateral, in USD with 6 decimals: + - opening or increasing: size x increase_position_bps / 10000 + - closing or liquidating: size x decrease_position_bps / 10000, rounded up + - both also pay a price impact fee of size x ceil(size x 10000 / + pricing.trade_impact_fee_scalar) / 10000. A lower scalar charges more. + + A keeper liquidates a position once collateral + PnL - close and borrow fees falls below + size x 10000 / pricing.max_leverage, so raising decrease_position_bps makes positions + liquidatable at an unchanged price. Opening a position whose close fee would exceed its + collateral fails with error 6022. + + EXAMPLE - charge 1% to open and to close SOL positions: + increase_position_bps: 100 + decrease_position_bps: 100 + + - id: jupiter-perps-pool-limits + name: Override JLP Pool Limits + description: Override the JLP pool's AUM cap, weight buffer and price guard + idl_account_name: Pool + properties: + - path: limit.max_aum_usd + label: Max AUM + description: "Deposits that would take the pool's value above this are refused, USD with 6 decimals. Example: \"3000000000000000\" is $3B" + - path: limit.token_weightage_buffer_bps + label: Weight buffer + description: "How far a token's value may exceed its target after a deposit, in BPS of that target. Example: \"1000\" is 10%" + - path: max_lp_token_price_change_bps + label: Max JLP price move + description: "Largest JLP price move from the pool's recorded value that deposits accept, in BPS. Example: 500 is 5%" + address: + type: pubkey + value: 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq + llm_context: | + Use this template to refuse or reopen JLP deposits. The address is the JLP Pool, the only + pool Jupiter Perps runs. Set fetchBeforeUse: true. limit.max_aum_usd and + limit.token_weightage_buffer_bps are u128: pass them as decimal strings. + + - limit.max_aum_usd: a deposit that would take the pool's value above it is refused + (error 6024). Below the pool's aum_usd, every deposit is refused. + - limit.token_weightage_buffer_bps: a deposit is refused (error 6043) when it leaves the + token's value above its target (the custody's target_ratio_bps of the pool) by more + than target x buffer / 10000. 0 refuses deposits of every token already above target. + - max_lp_token_price_change_bps: deposits are refused (error 6066) once a price shock + moves the JLP price more than this from the value the pool recorded. A shock of more + than about 5% of the JLP price trips the live 500; raise it to 10000 to keep deposits + open after a crash. + + EXAMPLE - refuse every JLP deposit: + limit.max_aum_usd: "1" + + - id: jupiter-perps-pool-fees + name: Override JLP Pool Fees + description: Override the JLP pool's deposit, withdrawal and swap fees + idl_account_name: Pool + properties: + - path: fees.add_remove_liquidity_bps + label: Deposit and withdrawal fee + description: "Base fee on JLP deposits and withdrawals, in BPS. Example: 20 is 0.2%" + - path: fees.swap_bps + label: Swap fee + description: "Base fee on swaps that involve a non-stable token, in BPS. Example: 10 is 0.1%" + - path: fees.tax_bps + label: Weight tax + description: "Raises the fee of deposits and swaps that move a token away from its target weight and lowers it for those that move it back, in BPS. Example: 100 is 1%" + - path: fees.stable_swap_bps + label: Stable swap fee + description: "Base fee on swaps between two stablecoins, in BPS. Example: 2 is 0.02%" + - path: fees.stable_swap_tax_bps + label: Stable swap weight tax + description: "The weight tax for swaps between two stablecoins, in BPS. Example: 5 is 0.05%" + address: + type: pubkey + value: 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq + llm_context: | + Use this template to change what JLP deposits, withdrawals and pool swaps cost. The + address is the JLP Pool, the only pool Jupiter Perps runs. Set fetchBeforeUse: true. + Every field is in BPS (10000 = 100%). + + - fees.add_remove_liquidity_bps: base fee on JLP deposits and withdrawals. + - fees.swap_bps: base fee on swaps that involve a non-stable token. That token's custody + multiplies the swap fee by 1 + its external_swap_fee_multiplier_bps / 10000, so at + 20000 a swap pays three times the fee. + - fees.stable_swap_bps: base fee on swaps between two stablecoins. + - fees.tax_bps and fees.stable_swap_tax_bps: the weight tax. A deposit or swap that moves + a token further from its target weight (the custody's target_ratio_bps of the pool) + pays more than the base fee, one that moves it back pays less, never below 0. At 0 + every deposit and swap pays exactly the base fee. + + getAddLiquidityAmountAndFee2 and getRemoveLiquidityAmountAndFee2 return the fee in BPS a + deposit or withdrawal would pay. + + EXAMPLE - a flat 1% on every JLP deposit and withdrawal: + fees.add_remove_liquidity_bps: 100 + fees.tax_bps: 0 diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index 4e2088488..675d1e297 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -68,6 +68,15 @@ pub const PHOENIX_ETERNAL_IDL_CONTENT: &str = pub const PHOENIX_ETERNAL_OVERRIDES_CONTENT: &str = include_str!("./protocols/phoenix-eternal/v1/overrides.yaml"); +pub const JUPITER_PERPS_IDL_CONTENT: &str = include_str!("./protocols/jupiter-perps/v1/idl.json"); +pub const JUPITER_PERPS_OVERRIDES_CONTENT: &str = + include_str!("./protocols/jupiter-perps/v1/overrides.yaml"); + +pub const JUPITER_PERPS_DOVES_IDL_CONTENT: &str = + include_str!("./protocols/jupiter-perps/doves/v1/idl.json"); +pub const JUPITER_PERPS_DOVES_OVERRIDES_CONTENT: &str = + include_str!("./protocols/jupiter-perps/doves/v1/overrides.yaml"); + /// Registry for managing override templates loaded from YAML files #[derive(Clone, Debug, Default)] pub struct TemplateRegistry { @@ -117,6 +126,7 @@ impl TemplateRegistry { default.load_spl_token_overrides(); default.load_pump_overrides(); default.load_phoenix_overrides(); + default.load_jupiter_perps_overrides(); default } @@ -224,6 +234,19 @@ impl TemplateRegistry { ); } + pub fn load_jupiter_perps_overrides(&mut self) { + self.load_protocol_overrides( + JUPITER_PERPS_IDL_CONTENT, + JUPITER_PERPS_OVERRIDES_CONTENT, + "jupiter-perps", + ); + self.load_protocol_overrides( + JUPITER_PERPS_DOVES_IDL_CONTENT, + JUPITER_PERPS_DOVES_OVERRIDES_CONTENT, + "jupiter-perps-doves", + ); + } + fn load_protocol_overrides( &mut self, idl_content: &str, @@ -606,11 +629,12 @@ mod tests { // Pyth (1) + Jupiter (1) + Raydium CLMM (1) + Raydium AMM v4 (4) + Drift (4) + Meteora (2) // + Kamino (Lend 17, Scope 3, Farms 5, Swap 2, Vault 5, Liquidity 4 = 36) - // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + Phoenix Eternal (19) = 81 + // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + Phoenix Eternal (19) + // + Jupiter Perps (5) + Doves (1) = 87 assert_eq!( registry.count(), - 81, - "Registry should load 81 templates total" + 87, + "Registry should load 87 templates total" ); assert!(registry.contains("pyth-price-feed-v2")); @@ -685,6 +709,13 @@ mod tests { assert!(registry.contains("phoenix-market-move")); assert!(registry.contains("phoenix-oracle-price")); assert!(registry.contains("phoenix-trader-collateral")); + + assert!(registry.contains("jupiter-perps-custody-risk")); + assert!(registry.contains("jupiter-perps-custody-borrow-rate")); + assert!(registry.contains("jupiter-perps-custody-fees")); + assert!(registry.contains("jupiter-perps-pool-limits")); + assert!(registry.contains("jupiter-perps-pool-fees")); + assert!(registry.contains("jupiter-perps-doves-price")); } #[test] @@ -1017,6 +1048,17 @@ templates: [] 19, "Should have 19 Phoenix Eternal templates" ); + + assert_eq!( + registry.by_protocol("jupiter-perps").len(), + 5, + "Should have 5 Jupiter Perps templates" + ); + assert_eq!( + registry.by_protocol("jupiter-perps-doves").len(), + 1, + "Should have 1 Doves template" + ); } #[test] @@ -1026,8 +1068,8 @@ templates: [] let oracle_templates = registry.by_tags(&[vec!["oracle".to_string()]].concat()); assert_eq!( oracle_templates.len(), - 4, - "Should find 4 oracle templates (Pyth + 3 Kamino Scope)" + 5, + "Should find 5 oracle templates (Pyth + 3 Kamino Scope + Doves)" ); let rewards_templates = registry.by_tags(&[vec!["rewards".to_string()]].concat()); diff --git a/crates/core/src/tests/jupiter_perps/mod.rs b/crates/core/src/tests/jupiter_perps/mod.rs new file mode 100644 index 000000000..0b2ff9c4a --- /dev/null +++ b/crates/core/src/tests/jupiter_perps/mod.rs @@ -0,0 +1,1352 @@ +//! Jupiter Perps integration tests. They read live mainnet accounts and run the deployed Perps +//! and Doves programs, so they need a network connection and `--features integration-tests`: +//! +//! ```text +//! cargo test -p surfpool-core --features integration-tests jupiter_perps -- --test-threads=1 +//! ``` +//! +//! `SURFPOOL_TEST_RPC_URL` overrides the endpoint. Instructions are built from the bundled IDL. The +//! local VM does not check signatures, so transactions run as the keeper with no key. + +use std::{ + collections::{HashMap, HashSet}, + sync::OnceLock, + time::{SystemTime, UNIX_EPOCH}, +}; + +use anchor_lang_idl::types::IdlInstructionAccountItem; +use litesvm::types::TransactionMetadata; +use serde_json::{Value, json}; +use solana_account::Account; +use solana_account_decoder::{UiAccountData, UiAccountEncoding, UiDataSliceConfig}; +use solana_client::rpc_filter::{Memcmp, RpcFilterType}; +use solana_clock::Clock; +use solana_commitment_config::CommitmentConfig; +use solana_compute_budget_interface::ComputeBudgetInstruction; +use solana_instruction::{AccountMeta, Instruction, error::InstructionError}; +use solana_message::{Message, VersionedMessage}; +use solana_program_option::COption; +use solana_program_pack::Pack; +use solana_pubkey::Pubkey; +use solana_rpc_client_api::config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; +use solana_sdk_ids::system_program; +use solana_signature::Signature; +use solana_transaction::versioned::VersionedTransaction; +use solana_transaction_error::TransactionError; +use spl_associated_token_account_interface::address::get_associated_token_address; +use spl_token_interface::state::{Account as TokenAccount, AccountState}; +use surfpool_types::{AccountAddress, OverrideInstance, Scenario}; +use txtx_addon_kit::types::types::Value as TxtxValue; +use txtx_addon_network_svm::codec::idl::IdlRef; +use txtx_addon_network_svm_types::idl::parse_bytes_to_value_with_expected_idl_type_def_ty; + +use crate::{ + scenarios::TemplateRegistry, + surfnet::{ + locker::SurfnetSvmLocker, + remote::SurfnetRemoteClient, + svm::{SurfnetSvm, get_txtx_value_json_converters}, + }, +}; + +const RPC_URL_ENV: &str = "SURFPOOL_TEST_RPC_URL"; +const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; + +const PERPS: Pubkey = Pubkey::from_str_const("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); +const PERPS_PROGRAMDATA: Pubkey = + Pubkey::from_str_const("C258LjtN8Q928yz2nJe5kwQycckGfWBSrhZBP2mTpLQt"); +const PERPS_DEPLOYED_SLOT: u64 = 413092478; +const DOVES: Pubkey = Pubkey::from_str_const("DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e"); +const DOVES_PROGRAMDATA: Pubkey = + Pubkey::from_str_const("4QRRLV8zvwvWDgPNWYXffcMLTS4M6UFCuj2pPMA1tg5D"); +const DOVES_DEPLOYED_SLOT: u64 = 439782203; + +const POOL: Pubkey = Pubkey::from_str_const("5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq"); +/// Set in the deployed bytecode, not in any account, so a rotation shows up only as +/// `InvalidKeeper`. +const KEEPER: Pubkey = Pubkey::from_str_const("Ag28fGtwtpnqassHURUBsQ1WfiyaWWzDDNs4Q28qHRjv"); +const CLOCK: Pubkey = Pubkey::from_str_const("SysvarC1ock11111111111111111111111111111111"); +const TOKEN_PROGRAM: Pubkey = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +const SOL_MINT: Pubkey = Pubkey::from_str_const("So11111111111111111111111111111111111111112"); +const BTC_MINT: Pubkey = Pubkey::from_str_const("3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh"); +const USDC_MINT: Pubkey = Pubkey::from_str_const("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +const JUPUSD_MINT: Pubkey = Pubkey::from_str_const("JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD"); +const SOL_USD_FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; + +const PRICE: &str = "jupiter-perps-doves-price"; +const RISK: &str = "jupiter-perps-custody-risk"; +const BORROW_RATE: &str = "jupiter-perps-custody-borrow-rate"; +const TRADING_FEES: &str = "jupiter-perps-custody-fees"; +const POOL_LIMITS: &str = "jupiter-perps-pool-limits"; +const POOL_FEES: &str = "jupiter-perps-pool-fees"; +const DEPOSIT_QUOTE: &str = "get_add_liquidity_amount_and_fee2"; + +/// Anchor's prefix on the self-invocation that carries an event. +const EVENT_IX_TAG: [u8; 8] = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d]; + +const STALE_ORACLE_PRICE: u32 = 6003; +const INVALID_POSITION_STATE: u32 = 6009; +const MAX_LEVERAGE: u32 = 6022; +const POOL_AMOUNT_LIMIT: u32 = 6024; +const INSTRUCTION_NOT_ALLOWED: u32 = 6027; +const INVALID_KEEPER: u32 = 6034; +const PULL_ORACLE_PUBLISH_TIME_TOO_EARLY: u32 = 6045; +const LP_TOKEN_PRICE_CHANGE_LIMIT_EXCEEDED: u32 = 6066; + +const BPS: u128 = 10_000; +const RATE_POWER: u128 = 1_000_000_000; +const DAY: i64 = 24 * 3600; +const ONE_SOL: u64 = 1_000_000_000; + +/// Byte offsets, discriminator included, of the fields these tests read. +mod custody { + pub const MINT: usize = 40; + pub const TOKEN_ACCOUNT: usize = 72; + pub const DECIMALS: usize = 104; + pub const ORACLE_ACCOUNT: usize = 106; + pub const TRADE_IMPACT_FEE_SCALAR: usize = 151; + pub const MAX_LEVERAGE: usize = 175; + pub const CUMULATIVE_INTEREST_RATE: usize = 262; + pub const INCREASE_POSITION_BPS: usize = 296; + pub const DECREASE_POSITION_BPS: usize = 304; + pub const DOVES_AG_ORACLE: usize = 384; + pub const EXTERNAL_SWAP_FEE_MULTIPLIER_BPS: usize = 1076; +} + +mod feed { + pub const PRICE: usize = 168; + pub const EXPO: usize = 176; + pub const TIMESTAMP: usize = 177; +} + +mod position { + pub const POOL: usize = 40; + pub const CUSTODY: usize = 72; + pub const COLLATERAL_CUSTODY: usize = 104; + pub const UPDATE_TIME: usize = 144; + pub const SIDE: usize = 152; + pub const PRICE: usize = 153; + pub const SIZE_USD: usize = 161; + pub const COLLATERAL_USD: usize = 169; + pub const CUMULATIVE_INTEREST_SNAPSHOT: usize = 185; + pub const LEN: usize = 216; + pub const LONG: u8 = 1; + pub const SHORT: u8 = 2; +} + +fn u64_at(data: &[u8], at: usize) -> u64 { + u64::from_le_bytes(data[at..at + 8].try_into().unwrap()) +} + +fn u128_at(data: &[u8], at: usize) -> u128 { + u128::from_le_bytes(data[at..at + 16].try_into().unwrap()) +} + +fn pubkey_at(data: &[u8], at: usize) -> Pubkey { + Pubkey::try_from(&data[at..at + 32]).unwrap() +} + +fn pubkey(json: &Value) -> Pubkey { + let address = json.as_str().and_then(|address| address.parse().ok()); + address.unwrap_or_else(|| panic!("{json} is not a pubkey")) +} + +fn pubkeys(json: &Value) -> Vec { + json.as_array().unwrap().iter().map(pubkey).collect() +} + +fn amount(json: &Value, field: &str) -> u64 { + let amount = json[field].as_u64(); + amount.unwrap_or_else(|| panic!("{field} is not an amount in {json}")) +} + +fn client() -> SurfnetRemoteClient { + SurfnetRemoteClient::new( + std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()), + ) +} + +/// Fetches the accounts in one request, so every account returned is from the same slot. +async fn fetch(addresses: &[Pubkey]) -> Vec { + client() + .get_multiple_accounts(addresses, CommitmentConfig::confirmed()) + .await + .unwrap_or_else(|e| panic!("failed to fetch {addresses:?} from mainnet: {e}")) + .into_iter() + .zip(addresses) + .map(|(result, address)| { + result.map_account().unwrap_or_else(|_| { + panic!("{address} no longer exists on mainnet; the test needs a new address") + }) + }) + .collect() +} + +/// The deployed bytecode, read from the upgradeable loader's ProgramData account once per test +/// process. The ELF starts 45 bytes in, past the loader header, whose slot field must match the +/// deploy these tests were written against. +async fn deployed_program(programdata: Pubkey, deployed_slot: u64) -> Vec { + static CACHE: OnceLock>>> = OnceLock::new(); + let mut cache = CACHE.get_or_init(Default::default).lock().await; + if let Some(bytes) = cache.get(&programdata) { + return bytes.clone(); + } + let data = fetch(&[programdata]).await.remove(0).data; + let slot = u64_at(&data, 4); + assert_eq!( + slot, deployed_slot, + "{programdata} was redeployed at slot {slot}; rerun these tests against it and update the \ + pinned slot" + ); + let bytes = data[45..].to_vec(); + cache.insert(programdata, bytes.clone()); + bytes +} + +fn pda(seeds: &[&[u8]]) -> Pubkey { + Pubkey::find_program_address(seeds, &PERPS).0 +} + +fn perpetuals_address() -> Pubkey { + pda(&[b"perpetuals"]) +} + +fn custody_address(mint: Pubkey) -> Pubkey { + pda(&[b"custody", POOL.as_ref(), mint.as_ref()]) +} + +fn jlp_mint() -> Pubkey { + pda(&[b"lp_token_mint", POOL.as_ref()]) +} + +/// The bundled Perps IDL, as the registry loads it. +fn perps_idl() -> &'static IdlRef { + static IDL: OnceLock = OnceLock::new(); + IDL.get_or_init(|| { + let registry = TemplateRegistry::new(); + IdlRef::from_idl(registry.get(RISK).unwrap().idl().clone()) + }) +} + +/// `bytes` decoded as the Perps IDL type `name`, rendered as `jsonParsed` renders it. +fn decode(name: &str, bytes: &[u8]) -> Value { + let idl = perps_idl(); + let ty = &idl.get_type(name).unwrap().ty; + parse_bytes_to_value_with_expected_idl_type_def_ty(bytes, ty, &idl.idl.types, &vec![], &vec![]) + .unwrap_or_else(|e| panic!("{name} does not decode: {e}")) + .to_json(Some(&get_txtx_value_json_converters())) +} + +/// Instruction params as the txtx value the IDL encoder takes. +fn txtx(json: &Value) -> TxtxValue { + match json { + Value::Null => TxtxValue::null(), + Value::Number(number) => TxtxValue::integer(number.as_u64().unwrap().into()), + Value::Object(fields) => TxtxValue::object( + fields + .iter() + .map(|(name, value)| (name.clone(), txtx(value))) + .collect(), + ), + _ => panic!("{json} is not a Perps instruction param"), + } +} + +/// The Perps instruction `name` from the bundled IDL: its discriminator, `params` encoded through +/// the IDL, and its accounts in IDL order with the IDL's signer and writable flags. An account +/// missing from `named` is a fixed address, or the feed or vault of the custody its name starts +/// with; the feed fills both oracle slots, as live keepers send it. +fn perps_ix( + graph: &PerpsGraph, + name: &str, + named: &[(&str, Pubkey)], + params: Value, +) -> Instruction { + let idl = perps_idl(); + let named: HashMap<&str, Pubkey> = named.iter().copied().collect(); + let custody_of = |role: &str| named[format!("{role}custody").as_str()]; + let address = |account: &str| match account { + _ if named.contains_key(account) => named[account], + "perpetuals" => perpetuals_address(), + "pool" => POOL, + "transfer_authority" => pda(&[b"transfer_authority"]), + "lp_token_mint" => jlp_mint(), + "event_authority" => pda(&[b"__event_authority"]), + "program" | "referral" => PERPS, + "token_program" => TOKEN_PROGRAM, + "associated_token_program" => spl_associated_token_account_interface::program::ID, + "system_program" => system_program::ID, + _ => match account.split_once("custody_") { + Some((role, "doves_price_account" | "pythnet_price_account")) => { + graph.feed_of(custody_of(role)) + } + Some((role, "token_account")) => { + pubkey_at(graph.data(&custody_of(role)), custody::TOKEN_ACCOUNT) + } + _ => panic!("{name}: no address for {account}"), + }, + }; + let instruction = idl.get_instruction(name).unwrap(); + let accounts = instruction.accounts.iter().map(|item| match item { + IdlInstructionAccountItem::Single(account) => AccountMeta { + pubkey: address(&account.name), + is_signer: account.signer, + is_writable: account.writable, + }, + IdlInstructionAccountItem::Composite(_) => panic!("{name} has nested accounts"), + }); + let mut data = instruction.discriminator.clone(); + data.extend(idl.get_encoded_args(name, vec![txtx(¶ms)]).unwrap()); + Instruction { + program_id: PERPS, + accounts: accounts.collect(), + data, + } +} + +/// The live accounts a Perps instruction reads, all from one slot, with that slot's clock. +#[derive(Clone)] +struct PerpsGraph { + clock: Clock, + accounts: HashMap, + /// The Pool's custodies, in the order the Pool lists them. + custodies: Vec, + /// Each custody's `doves_ag_oracle`, in the same order. + feeds: Vec, +} + +impl PerpsGraph { + fn data(&self, address: &Pubkey) -> &[u8] { + let missing = || panic!("{address} is not in the graph"); + &self.accounts.get(address).unwrap_or_else(missing).data + } + + fn feed_of(&self, custody: Pubkey) -> Pubkey { + pubkey_at(self.data(&custody), custody::DOVES_AG_ORACLE) + } + + /// The feed's price in USD with 6 decimals, the unit positions use. + fn feed_usd(&self, feed: Pubkey) -> u128 { + let data = self.data(&feed); + usd(u64_at(data, feed::PRICE), data[feed::EXPO] as i8) + } + + /// `usd` (6 decimals) in the smallest unit of the token, at its feed's price. + fn tokens_worth(&self, mint: Pubkey, usd: u128) -> u64 { + let custody = custody_address(mint); + let decimals = self.data(&custody)[custody::DECIMALS] as u32; + u64::try_from(usd * 10u128.pow(decimals) / self.feed_usd(self.feed_of(custody))).unwrap() + } + + /// Every custody, then every feed: the accounts Perps values the whole pool from. + fn pool_remaining_accounts(&self) -> Vec { + let all = self.custodies.iter().chain(&self.feeds); + all.map(|address| AccountMeta::new_readonly(*address, false)) + .collect() + } +} + +/// Reads the Perpetuals and Pool accounts, every custody with its feed, vault, mint and Pyth +/// account, the JLP mint, `extra` and the clock in one request. +async fn perps_graph(extra: &[Pubkey]) -> PerpsGraph { + let custodies = pubkeys(&decode("Pool", &fetch(&[POOL]).await[0].data[8..])["custodies"]); + let custody_accounts = fetch(&custodies).await; + let field = |at| -> Vec { + let accounts = custody_accounts.iter(); + accounts + .map(|custody| pubkey_at(&custody.data, at)) + .collect() + }; + let feeds = field(custody::DOVES_AG_ORACLE); + let mut addresses = vec![perpetuals_address(), POOL, jlp_mint()]; + addresses.extend(custodies.iter().chain(&feeds).chain(extra)); + for at in [ + custody::TOKEN_ACCOUNT, + custody::MINT, + custody::ORACLE_ACCOUNT, + ] { + addresses.extend(field(at)); + } + addresses.sort_unstable(); + addresses.dedup(); + addresses.push(CLOCK); + let mut fetched = fetch(&addresses).await; + let clock: Clock = bincode::deserialize(&fetched.pop().unwrap().data).unwrap(); + addresses.pop(); + PerpsGraph { + clock, + accounts: addresses.into_iter().zip(fetched).collect(), + custodies, + feeds, + } +} + +/// A local VM holding every graph account at the graph's clock, with no programs loaded. +fn local_vm(graph: &PerpsGraph) -> SurfnetSvmLocker { + let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let locker = SurfnetSvmLocker::new(svm); + locker.with_svm_writer(|svm| { + svm.inner.set_sysvar(&graph.clock); + for (address, account) in &graph.accounts { + svm.set_account(address, account.clone()).unwrap(); + } + }); + locker +} + +/// The local VM with the deployed Perps and Doves programs and a funded keeper. +async fn perps_vm(graph: &PerpsGraph) -> SurfnetSvmLocker { + let perps = deployed_program(PERPS_PROGRAMDATA, PERPS_DEPLOYED_SLOT).await; + let doves = deployed_program(DOVES_PROGRAMDATA, DOVES_DEPLOYED_SLOT).await; + let locker = local_vm(graph); + locker.with_svm_writer(|svm| { + svm.inner.svm.add_program(PERPS, &perps).unwrap(); + svm.inner.svm.add_program(DOVES, &doves).unwrap(); + }); + fund(&locker, KEEPER); + locker +} + +fn set_account(locker: &SurfnetSvmLocker, address: Pubkey, account: Account) { + locker.with_svm_writer(|svm| svm.set_account(&address, account).unwrap()); +} + +fn fund(locker: &SurfnetSvmLocker, address: Pubkey) { + let account = Account::new(100_000_000_000, 0, &system_program::ID); + set_account(locker, address, account); +} + +fn set_clock(locker: &SurfnetSvmLocker, unix_timestamp: i64) { + let mut clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); + clock.unix_timestamp = unix_timestamp; + locker.with_svm_writer(|svm| svm.inner.set_sysvar(&clock)); +} + +fn local(locker: &SurfnetSvmLocker, address: &Pubkey) -> Option { + locker.with_svm_reader(|svm| svm.get_account(address).unwrap()) +} + +fn account(locker: &SurfnetSvmLocker, address: &Pubkey) -> Account { + let missing = || panic!("{address} is missing from the local VM"); + local(locker, address).unwrap_or_else(missing) +} + +/// Every Doves feed as if copied when it published at `timestamp`. +fn copy_feeds_at(locker: &SurfnetSvmLocker, graph: &PerpsGraph, timestamp: i64) { + for feed in &graph.feeds { + let mut copied = account(locker, feed); + copied.data[feed::TIMESTAMP..feed::TIMESTAMP + 8].copy_from_slice(×tamp.to_le_bytes()); + set_account(locker, *feed, copied); + } +} + +/// The account as a `jsonParsed` read returns it, decoded through the bundled IDLs. +fn parsed(locker: &SurfnetSvmLocker, address: Pubkey) -> Value { + let encoding = UiAccountEncoding::JsonParsed; + let ui = locker.encode_ui_account(&address, &account(locker, &address), encoding, None, None); + let UiAccountData::Json(parsed) = ui.data else { + panic!("{address} did not decode as JSON"); + }; + parsed.parsed +} + +/// Plays the overrides through the scenario materializer at the graph's slot, then reads each +/// pubkey-addressed target back as `jsonParsed` and checks the last value given for each field. +async fn play(locker: &SurfnetSvmLocker, graph: &PerpsGraph, overrides: Vec) { + let mut scenario = Scenario::new("Jupiter Perps".into(), "State a test prepares".into()); + for instance in &overrides { + scenario.add_override(instance.clone()); + } + let slot = graph.clock.slot; + locker.register_scenario(scenario, Some(slot)).unwrap(); + let materialized = locker.materialize_overrides_for_slot(&None, slot); + materialized.await.unwrap(); + let mut checked = HashSet::new(); + for instance in overrides.iter().rev() { + let AccountAddress::Pubkey(address) = &instance.account else { + continue; + }; + let json = parsed(locker, address.parse().unwrap()); + for (path, value) in &instance.values { + if checked.insert((address, path)) { + let landed = path.split('.').fold(&json, |json, key| &json[key]); + let what = format!("{} on {address}: {path}", instance.template_id); + assert_eq!(landed, value, "{what}"); + } + } + } +} + +/// An override of `template_id` on `address`; `values` is a JSON object of field paths. +fn template_override(template_id: &str, address: Pubkey, values: Value) -> OverrideInstance { + let account = AccountAddress::Pubkey(address.to_string()); + let instance = OverrideInstance::new(template_id.to_string(), 0, account); + instance.with_values(serde_json::from_value(values).unwrap()) +} + +fn price_override(feed: Pubkey, price: u64) -> OverrideInstance { + template_override(PRICE, feed, json!({ "price": price })) +} + +/// The SOL feed at its live price: an override that moves nothing. +fn live_price(graph: &PerpsGraph) -> OverrideInstance { + let feed = graph.feed_of(custody_address(SOL_MINT)); + price_override(feed, u64_at(graph.data(&feed), feed::PRICE)) +} + +fn timestamp_override(feed: Pubkey, timestamp: i64) -> OverrideInstance { + template_override(PRICE, feed, json!({ "timestamp": timestamp })) +} + +fn pyth_override() -> OverrideInstance { + let registry = TemplateRegistry::new(); + let template = registry.get("pyth-price-feed-v2").unwrap().clone(); + let values = json!({ "feed_id": SOL_USD_FEED_ID, "price_message.price": 1_000_000 }); + OverrideInstance::new(template.id, 0, template.address) + .with_values(serde_json::from_value(values).unwrap()) +} + +/// A feed mantissa at a negative `expo` as USD with 6 decimals, the unit positions use. +fn usd(price: u64, expo: i8) -> u128 { + price as u128 * 1_000_000 / 10u128.pow(-expo as u32) +} + +/// USD with 6 decimals as a feed mantissa at a negative `expo`. +fn mantissa(usd: u128, expo: i8) -> u64 { + u64::try_from(usd * 10u128.pow(-expo as u32) / 1_000_000).unwrap() +} + +type Failed = (TransactionError, Vec); +type Sent = Result; + +/// Sends the instructions with `signer` as fee payer and every other signer unchecked: the local +/// VM runs with signature verification off, as `--skip-signature-verification` does. +fn send(locker: &SurfnetSvmLocker, signer: Pubkey, instructions: &[Instruction]) -> Sent { + locker.with_svm_writer(|svm| { + let mut all = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)]; + all.extend_from_slice(instructions); + let message = Message::new_with_blockhash(&all, Some(&signer), &svm.latest_blockhash()); + let signers = message.header.num_required_signatures; + let transaction = VersionedTransaction { + signatures: (0..signers).map(|_| Signature::new_unique()).collect(), + message: VersionedMessage::Legacy(message), + }; + svm.send_transaction(transaction, false, false) + .map_err(|failed| (failed.err, failed.meta.logs)) + }) +} + +fn custom_error(sent: &Result) -> Option { + match sent { + Err((TransactionError::InstructionError(_, InstructionError::Custom(code)), _)) => { + Some(*code) + } + _ => None, + } +} + +fn expect_error(sent: Result, code: u32, what: &str) { + let got = custom_error(&sent); + assert_eq!(got, Some(code), "{what}: got {sent:?}"); +} + +/// The Perps event of that name the transaction emitted, decoded through the bundled IDL. +fn event(meta: &TransactionMetadata, name: &str) -> Value { + let event = perps_idl().idl.events.iter().find(|e| e.name == name); + let discriminator = &event.unwrap().discriminator; + let data = meta + .inner_instructions + .iter() + .flatten() + .map(|inner| &inner.instruction.data) + .find(|data| data.starts_with(&EVENT_IX_TAG) && data[8..16] == discriminator[..]) + .unwrap_or_else(|| panic!("the transaction emitted no {name}")); + decode(name, &data[16..]) +} + +/// A JLP deposit or withdrawal quote: the `AmountAndFee` it returns. +fn quote( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + name: &str, + named: &[(&str, Pubkey)], + params: Value, +) -> Result { + let mut instruction = perps_ix(graph, name, named, params); + instruction.accounts.extend(graph.pool_remaining_accounts()); + let returned = send(locker, KEEPER, &[instruction])?.return_data.data; + Ok(decode("AmountAndFee", &returned)) +} + +fn deposit_quote( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + mint: Pubkey, + amount: u64, +) -> Result { + let named = [("custody", custody_address(mint))]; + let params = json!({ "token_amount_in": amount }); + quote(locker, graph, DEPOSIT_QUOTE, &named, params) +} + +fn liquidate(locker: &SurfnetSvmLocker, graph: &PerpsGraph, signer: Pubkey, at: Pubkey) -> Sent { + let data = account(locker, &at).data; + let custody = pubkey_at(&data, position::CUSTODY); + let collateral = pubkey_at(&data, position::COLLATERAL_CUSTODY); + let named = [ + ("signer", signer), + ("position", at), + ("custody", custody), + ("collateral_custody", collateral), + ]; + let instruction = perps_ix(graph, "liquidate_full_position4", &named, json!({})); + send(locker, signer, &[instruction]) +} + +/// The keeper's liquidation, which must close the position. +fn liquidated( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + at: Pubkey, + what: &str, +) -> TransactionMetadata { + let meta = liquidate(locker, graph, KEEPER, at) + .unwrap_or_else(|e| panic!("{what} was not liquidated: {e:?}")); + let size = u64_at(&account(locker, &at).data, position::SIZE_USD); + assert_eq!(size, 0, "{what}: the liquidated position still has a size"); + meta +} + +/// One kind of position: the market it trades, the custody holding its collateral and its side. +struct Market { + label: &'static str, + mint: Pubkey, + collateral_mint: Pubkey, + side: u8, +} + +const SOL_LONG: Market = Market { + label: "SOL long", + mint: SOL_MINT, + collateral_mint: SOL_MINT, + side: position::LONG, +}; + +const BTC_SHORT: Market = Market { + label: "BTC short with USDC collateral", + mint: BTC_MINT, + collateral_mint: USDC_MINT, + side: position::SHORT, +}; + +/// Open positions of the market, newest updates first. A position account outlives its trade with +/// a zero size, so the scan walks back through `update_time` in 2^16-second windows and keeps the +/// open ones. +async fn open_positions(market: &Market, wanted: usize) -> Vec { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + let position_account = perps_idl().get_account("Position").unwrap(); + let mut pool_custody_collateral = POOL.to_bytes().to_vec(); + pool_custody_collateral.extend(custody_address(market.mint).to_bytes()); + pool_custody_collateral.extend(custody_address(market.collateral_mint).to_bytes()); + let mut found = Vec::new(); + for window in (0..16).map(|back| (now.as_secs() >> 16) - back) { + let mut window_and_side = window.to_le_bytes()[..6].to_vec(); + window_and_side.push(market.side); + let memcmp = [ + (0, position_account.discriminator.clone()), + (position::POOL, pool_custody_collateral.clone()), + (position::UPDATE_TIME + 2, window_and_side), + ]; + let memcmp = memcmp.map(|(at, bytes)| Memcmp::new_raw_bytes(at, bytes)); + let mut filters: Vec<_> = memcmp.into_iter().map(RpcFilterType::Memcmp).collect(); + filters.push(RpcFilterType::DataSize(position::LEN as u64)); + let size_only = UiDataSliceConfig { + offset: position::SIZE_USD, + length: 8, + }; + let config = RpcProgramAccountsConfig { + filters: Some(filters), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: Some(size_only), + commitment: Some(CommitmentConfig::confirmed()), + ..Default::default() + }, + ..Default::default() + }; + let accounts = client() + .client + .get_program_ui_accounts_with_config(&PERPS, config) + .await + .unwrap_or_else(|e| panic!("{} scan: {e}", market.label)); + let is_open = |size: Vec| u64_at(&size, 0) > 0; + let mut open: Vec = accounts + .into_iter() + .filter(|(_, account)| account.data.decode().is_some_and(is_open)) + .map(|(address, _)| address) + .collect(); + open.sort_unstable(); + found.extend(open); + if found.len() >= wanted { + break; + } + } + found.truncate(wanted); + found +} + +/// What a position keeps once its loss at `price` and its close and borrow fees are paid, and the +/// `size / max_leverage` it must keep, both USD with 6 decimals, by the liquidation price formula +/// of Jupiter's published examples. +fn margin(graph: &PerpsGraph, position_data: &[u8], price: u128) -> (i128, i128) { + let size = u64_at(position_data, position::SIZE_USD) as u128; + let collateral = u64_at(position_data, position::COLLATERAL_USD) as i128; + let entry = u64_at(position_data, position::PRICE) as u128; + let market = graph.data(&pubkey_at(position_data, position::CUSTODY)); + let collateral_custody = graph.data(&pubkey_at(position_data, position::COLLATERAL_CUSTODY)); + let impact_fee_bps = + (size * BPS).div_ceil(u64_at(market, custody::TRADE_IMPACT_FEE_SCALAR) as u128); + let close_fee = + size * (u64_at(market, custody::DECREASE_POSITION_BPS) as u128 + impact_fee_bps) / BPS; + let borrow_fee = (u128_at(collateral_custody, custody::CUMULATIVE_INTEREST_RATE) + - u128_at(position_data, position::CUMULATIVE_INTEREST_SNAPSHOT)) + * size + / RATE_POWER; + let move_usd = (size * price.abs_diff(entry) / entry) as i128; + let gain = match (position_data[position::SIDE], price >= entry) { + (position::LONG, true) | (position::SHORT, false) => move_usd, + _ => -move_usd, + }; + let left = collateral + gain - (close_fee + borrow_fee) as i128; + let required = (size * BPS / u64_at(market, custody::MAX_LEVERAGE) as u128) as i128; + (left, required) +} + +/// The feed price, USD with 6 decimals, at which `margin` runs out. +fn liquidation_price(graph: &PerpsGraph, position_data: &[u8]) -> Option { + let size = u64_at(position_data, position::SIZE_USD) as i128; + let entry = u64_at(position_data, position::PRICE) as i128; + let (left_at_entry, required) = margin(graph, position_data, entry as u128); + let room = left_at_entry - required; + let price = match position_data[position::SIDE] { + position::LONG => entry - room * entry / size, + _ => entry + room * entry / size, + }; + u128::try_from(price).ok().filter(|price| *price > 0) +} + +/// The first open position of the market that the deployed program calls healthy and whose +/// liquidation price, from the fields it reads, sits at least 5% from the live price. +async fn liquidation_candidate(market: &Market) -> (PerpsGraph, Pubkey, u128) { + let positions = open_positions(market, 30).await; + let graph = perps_graph(&positions).await; + let locker = perps_vm(&graph).await; + let price = graph.feed_usd(graph.feed_of(custody_address(market.mint))); + let mut rejected = Vec::new(); + for position_address in &positions { + let data = graph.data(position_address); + let Some(liquidation) = liquidation_price(&graph, data) else { + rejected.push(format!("{position_address}: no liquidation price")); + continue; + }; + let room = match market.side { + position::LONG => price * 95 >= liquidation * 100, + _ => price * 105 <= liquidation * 100, + }; + if !room { + rejected.push(format!( + "{position_address}: liquidation at {liquidation} is within 5% of {price}" + )); + continue; + } + let baseline = liquidate(&locker, &graph, KEEPER, *position_address); + if custom_error(&baseline) == Some(INVALID_POSITION_STATE) { + return (graph, *position_address, liquidation); + } + rejected.push(format!( + "{position_address}: baseline liquidation returned {:?}", + baseline.map(|_| ()).map_err(|(error, _)| error) + )); + } + panic!( + "no eligible live candidate: no open {} out of {} is healthy with room to move: {rejected:?}", + market.label, + positions.len() + ) +} + +/// A local VM on which the keeper cannot liquidate the position with `healthy` played, returned +/// with `stress` played on top. +async fn stressed( + graph: &PerpsGraph, + at: Pubkey, + healthy: Vec, + stress: Vec, + what: &str, +) -> SurfnetSvmLocker { + let locker = perps_vm(graph).await; + play(&locker, graph, healthy).await; + let refused = liquidate(&locker, graph, KEEPER, at); + expect_error(refused, INVALID_POSITION_STATE, what); + play(&locker, graph, stress).await; + locker +} + +/// The position's feed 1% short of its liquidation price, then 3% past it. +async fn price_shocked( + graph: &PerpsGraph, + market: &Market, + at: Pubkey, + liquidation: u128, +) -> SurfnetSvmLocker { + let feed = graph.feed_of(custody_address(market.mint)); + let expo = graph.data(&feed)[feed::EXPO] as i8; + let (short_of, past) = match market.side { + position::LONG => (liquidation * 101 / 100, liquidation * 97 / 100), + _ => (liquidation * 99 / 100, liquidation * 103 / 100), + }; + let shock = |price: u128| vec![price_override(feed, mantissa(price, expo))]; + let what = format!("{} 1% short of its liquidation price", market.label); + stressed(graph, at, shock(short_of), shock(past), &what).await +} + +/// An account of `owner` holding `amount` of `mint`. Wrapped SOL also holds the lamports, as a +/// native account does. +fn token_account(locker: &SurfnetSvmLocker, owner: Pubkey, mint: Pubkey, amount: u64) -> Pubkey { + const RENT: u64 = 2_039_280; + let native = mint == SOL_MINT; + let is_native = COption::from(native.then_some(RENT)); + let state = AccountState::Initialized; + let token = TokenAccount { + mint, + owner, + amount, + state, + is_native, + ..Default::default() + }; + let lamports = if native { RENT + amount } else { RENT }; + let mut account = Account::new(lamports, TokenAccount::LEN, &TOKEN_PROGRAM); + token.pack_into_slice(&mut account.data); + let address = Pubkey::new_unique(); + set_account(locker, address, account); + address +} + +fn token_amount_in(data: &[u8]) -> u64 { + TokenAccount::unpack(data).unwrap().amount +} + +fn token_amount(locker: &SurfnetSvmLocker, address: Pubkey) -> u64 { + token_amount_in(&account(locker, &address).data) +} + +/// A funded owner with `amount` of `mint` and an empty JLP account. +fn depositor(locker: &SurfnetSvmLocker, mint: Pubkey, amount: u64) -> (Pubkey, Pubkey, Pubkey) { + let owner = Pubkey::new_unique(); + fund(locker, owner); + let funding = token_account(locker, owner, mint, amount); + (owner, funding, token_account(locker, owner, jlp_mint(), 0)) +} + +/// `addLiquidity2` of `amount` of `mint` from the depositor's account. +fn add_liquidity( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + (owner, funding, jlp_account): (Pubkey, Pubkey, Pubkey), + mint: Pubkey, + amount: u64, +) -> Sent { + let named = [ + ("owner", owner), + ("funding_account", funding), + ("lp_token_account", jlp_account), + ("custody", custody_address(mint)), + ]; + let params = json!({ + "token_amount_in": amount, "min_lp_amount_out": 1, "token_amount_pre_swap": null, + }); + let mut instruction = perps_ix(graph, "add_liquidity2", &named, params); + instruction.accounts.extend(graph.pool_remaining_accounts()); + send(locker, owner, &[instruction]) +} + +/// `swap2` of `usd` (6 decimals) worth of `from` into `to` by a new trader: the fee in BPS it paid. +fn swap_fee( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + from: Pubkey, + to: Pubkey, + usd: u128, +) -> u64 { + let owner = Pubkey::new_unique(); + fund(locker, owner); + let amount_in = graph.tokens_worth(from, usd); + let funding = token_account(locker, owner, from, amount_in); + let receiving = token_account(locker, owner, to, 0); + let named = [ + ("owner", owner), + ("funding_account", funding), + ("receiving_account", receiving), + ("receiving_custody", custody_address(from)), + ("dispensing_custody", custody_address(to)), + ]; + let params = json!({ "amount_in": amount_in, "min_amount_out": 1 }); + let mut instruction = perps_ix(graph, "swap2", &named, params); + instruction.accounts.extend(graph.pool_remaining_accounts()); + let sent = send(locker, owner, &[instruction]); + let sent = sent.unwrap_or_else(|e| panic!("swap {from} to {to} failed: {e:?}")); + amount(&event(&sent, "PoolSwapEvent"), "fee_bps") +} + +fn position_of(trader: Pubkey, market: &Market) -> Pubkey { + let custody = custody_address(market.mint); + let collateral = custody_address(market.collateral_mint); + let side = [market.side]; + pda(&[ + b"position", + trader.as_ref(), + POOL.as_ref(), + custody.as_ref(), + collateral.as_ref(), + &side, + ]) +} + +/// The trader's market request for a `size_usd` position on $100 of collateral, as the app sends +/// it, the keeper's `increasePosition4` that fills it, and the position. +fn request_position( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + market: &Market, + trader: Pubkey, + size_usd: u64, +) -> (Sent, Instruction, Pubkey) { + let at = position_of(trader, market); + let mint = market.collateral_mint; + let custody = custody_address(market.mint); + let request = pda(&[b"position_request", at.as_ref(), &[0; 8], &[1]]); + let shared = [ + ("position", at), + ("position_request", request), + ( + "position_request_ata", + get_associated_token_address(&request, &mint), + ), + ("custody", custody), + ("collateral_custody", custody_address(mint)), + ]; + let collateral = graph.tokens_worth(mint, 100_000_000); + fund(locker, trader); + let funding = token_account(locker, trader, mint, collateral); + let price = graph.feed_usd(graph.feed_of(custody)) as u64; + let (side, worst_price) = match market.side { + position::LONG => ("Long", price * 2), + _ => ("Short", price / 2), + }; + let params = json!({ + "size_usd_delta": size_usd, "collateral_token_delta": collateral, "side": { side: null }, + "price_slippage": worst_price, "jupiter_minimum_out": null, "counter": 0, + }); + let owner = [ + ("owner", trader), + ("funding_account", funding), + ("input_mint", mint), + ]; + let named = [&owner[..], &shared].concat(); + let name = "create_increase_position_market_request"; + let requested = send(locker, trader, &[perps_ix(graph, name, &named, params)]); + let named = [&[("keeper", KEEPER)][..], &shared].concat(); + let fill = perps_ix(graph, "increase_position4", &named, json!({})); + (requested, fill, at) +} + +/// Opens a position as the app does: the request, then the keeper's fill one second later, since +/// a request cannot run in the second it was made, on a price the scenario publishes after the +/// request. Returns the first failure or the keeper's transaction, and the position. +async fn open_position( + locker: &SurfnetSvmLocker, + graph: &PerpsGraph, + market: &Market, + trader: Pubkey, + size_usd: u64, +) -> (Sent, Pubkey) { + let (requested, fill, at) = request_position(locker, graph, market, trader, size_usd); + if requested.is_err() { + return (requested, at); + } + let clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); + set_clock(locker, clock.unix_timestamp + 1); + let feed = graph.feed_of(custody_address(market.mint)); + play( + locker, + graph, + vec![timestamp_override(feed, clock.unix_timestamp + 1)], + ) + .await; + (send(locker, KEEPER, &[fill]), at) +} + +/// A failure here means a bundled IDL disagrees with the live layout. +#[tokio::test(flavor = "multi_thread")] +async fn bundled_idls_round_trip_live_accounts() { + let first_open = |positions: Vec, market: &Market| { + let none = || panic!("no eligible live candidate: no open {}", market.label); + *positions.first().unwrap_or_else(none) + }; + let long = first_open(open_positions(&SOL_LONG, 1).await, &SOL_LONG); + let short = first_open(open_positions(&BTC_SHORT, 1).await, &BTC_SHORT); + let graph = perps_graph(&[long, short]).await; + let (sol, btc) = (custody_address(SOL_MINT), custody_address(BTC_MINT)); + + let registry = TemplateRegistry::new(); + let perps_idl = &perps_idl().idl; + let doves_idl = registry.get(PRICE).unwrap().idl(); + let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + for (name, address, idl) in [ + ("Perpetuals", perpetuals_address(), perps_idl), + ("Pool", POOL, perps_idl), + ("Custody", sol, perps_idl), + ("Custody", btc, perps_idl), + ("Position", long, perps_idl), + ("Position", short, perps_idl), + ("AgPriceFeed", graph.feed_of(sol), doves_idl), + ("AgPriceFeed", graph.feed_of(btc), doves_idl), + ] { + let data = graph.data(&address); + let definition = idl.accounts.iter().find(|account| account.name == name); + let discriminator = &definition.unwrap().discriminator[..]; + assert_eq!(&data[..8], discriminator, "{address} is not a {name}"); + let forged = svm.get_forged_account_data(&address, data, idl, &HashMap::new()); + let forged = forged.unwrap_or_else(|e| panic!("{name} {address} does not decode: {e}")); + assert_eq!(forged, data, "{name} {address} changed on a round trip"); + } +} + +/// The lookup the templates' `llm_context` teaches, through `jsonParsed` reads: the one Pool, its +/// `custodies`, then each custody's `mint` and `doves_ag_oracle`. An account with bytes past its +/// IDL layout does not decode, so every custody and feed is read. +#[tokio::test(flavor = "multi_thread")] +async fn the_lookup_recipe_reads_every_market_as_json() { + let graph = perps_graph(&[]).await; + let locker = local_vm(&graph); + assert_eq!(pda(&[b"pool", b"Pool"]), POOL); + let pools = pubkeys(&parsed(&locker, perpetuals_address())["pools"]); + assert_eq!(pools, [POOL], "Perps now runs more than one pool"); + let registry = TemplateRegistry::new(); + for protocol in ["jupiter-perps", "jupiter-perps-doves"] { + for template in registry.by_protocol(protocol) { + let recipe = template.llm_context.as_deref().unwrap_or_default(); + match &template.address { + AccountAddress::Pubkey(literal) if !literal.is_empty() => { + assert_eq!(literal, &POOL.to_string(), "{}", template.id) + } + _ => assert!(recipe.contains(&POOL.to_string()), "{} lookup", template.id), + } + } + } + let custodies = pubkeys(&parsed(&locker, POOL)["custodies"]); + assert!(custodies.len() >= 3); + for custody in custodies { + let custody_json = parsed(&locker, custody); + let mint = pubkey(&custody_json["mint"]); + assert_eq!(custody, custody_address(mint)); + let feed = pubkey(&custody_json["doves_ag_oracle"]); + let ag_feed = Pubkey::find_program_address(&[b"ag_feed", mint.as_ref()], &DOVES).0; + assert_eq!(feed, ag_feed); + assert_eq!(account(&locker, &feed).owner, DOVES); + assert_eq!(pubkey(&parsed(&locker, feed)["mint"]), mint); + } +} + +/// Perps prices JLP from the Doves feed and never from the custody's Pyth account, even when the +/// Pyth account sits in the instruction's Pythnet slot. +#[tokio::test(flavor = "multi_thread")] +async fn doves_price_moves_the_jlp_quote_and_pyth_does_not() { + let graph = perps_graph(&[]).await; + let sol = custody_address(SOL_MINT); + let feed = graph.feed_of(sol); + let pyth = pubkey_at(graph.data(&sol), custody::ORACLE_ACCOUNT); + let locker = perps_vm(&graph).await; + play(&locker, &graph, vec![live_price(&graph)]).await; + let quote_with = |pythnet: Pubkey| { + let named = [("custody", sol), ("custody_pythnet_price_account", pythnet)]; + let params = json!({ "token_amount_in": ONE_SOL }); + let quoted = quote(&locker, &graph, DEPOSIT_QUOTE, &named, params); + quoted.unwrap_or_else(|e| panic!("quote failed: {e:?}")) + }; + let baseline = quote_with(feed); + + play(&locker, &graph, vec![pyth_override()]).await; + let pyth_price = &parsed(&locker, pyth)["price_message"]["price"]; + assert_eq!(pyth_price, 1_000_000, "the Pyth override missed"); + for pythnet in [feed, pyth] { + assert_eq!(quote_with(pythnet), baseline, "a Pyth price moved JLP"); + } + + let shocked = u64_at(graph.data(&feed), feed::PRICE) * 8 / 10; + play(&locker, &graph, vec![price_override(feed, shocked)]).await; + let quoted = quote_with(feed); + assert!( + amount("ed, "amount") < amount(&baseline, "amount"), + "a 20% lower SOL price should mint less JLP per SOL: {quoted} vs {baseline}" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn a_price_shock_liquidates_a_usdc_backed_btc_short() { + let (graph, at, liquidation) = liquidation_candidate(&BTC_SHORT).await; + let locker = price_shocked(&graph, &BTC_SHORT, at, liquidation).await; + liquidated(&locker, &graph, at, "a BTC short 3% past"); +} + +/// A keeper liquidates once `collateral + PnL - close and borrow fees` falls below +/// `size x 10^4 / pricing.max_leverage`, and no one else may. Each lever moves a healthy SOL long +/// short of that line, then past it: the price, `max_leverage` (half, then twice its margin), the +/// close fee (past its spare margin, not its margin) and the borrow rate (twice its spare margin +/// over a day, accrued by the liquidation itself). +#[tokio::test(flavor = "multi_thread")] +async fn each_lever_liquidates_a_sol_long() { + let (graph, at, liquidation) = liquidation_candidate(&SOL_LONG).await; + let sol = custody_address(SOL_MINT); + let size = u64_at(graph.data(&at), position::SIZE_USD) as i128; + let (left, required) = margin(&graph, graph.data(&at), graph.feed_usd(graph.feed_of(sol))); + let lever = |template: &str, values: Value| vec![template_override(template, sol, values)]; + + let locker = price_shocked(&graph, &SOL_LONG, at, liquidation).await; + let stranger = Pubkey::new_unique(); + fund(&locker, stranger); + let refused = liquidate(&locker, &graph, stranger, at); + expect_error(refused, INVALID_KEEPER, "another signer"); + liquidated(&locker, &graph, at, "3% past"); + + let risk = |requirement: i128| { + let max_leverage = u64::try_from(size * BPS as i128 / requirement).unwrap(); + lever(RISK, json!({ "pricing.max_leverage": max_leverage })) + }; + let (healthy, stress) = (risk(left / 2), risk(left * 2)); + let locker = stressed(&graph, at, healthy, stress, "half its margin").await; + liquidated(&locker, &graph, at, "twice its margin"); + + let live_fee = u64_at(graph.data(&sol), custody::DECREASE_POSITION_BPS); + let raised = live_fee + u64::try_from((left - required / 2) * BPS as i128 / size).unwrap(); + let close_fee = lever(TRADING_FEES, json!({ "decrease_position_bps": raised })); + let locker = stressed(&graph, at, vec![], close_fee, "the live close fee").await; + let what = format!("a {raised} BPS close fee"); + let closed = event( + &liquidated(&locker, &graph, at, &what), + "LiquidateFullPositionEvent", + ); + let fee = (size as u128 * raised as u128).div_ceil(BPS) as u64; + let impact = amount(&closed, "price_impact_fee_usd"); + assert_eq!(amount(&closed, "position_fee_usd"), fee + impact, "{what}"); + + let yearly_bps = u64::try_from(2 * (left - required) * BPS as i128 * 365 / size).unwrap(); + let mut later = graph.clone(); + later.clock.unix_timestamp += DAY; + let rates = json!({ + "jump_rate_state.min_rate_bps": yearly_bps, + "jump_rate_state.target_rate_bps": yearly_bps, + "jump_rate_state.max_rate_bps": yearly_bps, + }); + let rates = lever(BORROW_RATE, rates); + let locker = stressed(&later, at, vec![], rates, "a day at the live rate").await; + let what = format!("a day at {yearly_bps} BPS a year"); + liquidated(&locker, &later, at, &what); + let accrued = account(&locker, &sol).data; + let accrued = u128_at(&accrued, custody::CUMULATIVE_INTEREST_RATE); + let live = u128_at(graph.data(&sol), custody::CUMULATIVE_INTEREST_RATE); + assert!(accrued > live, "{what}: the borrow fee did not accrue"); +} + +/// Opening pays `size x increase_position_bps / 10^4` and a price impact fee of +/// `size x ceil(size x 10^4 / pricing.trade_impact_fee_scalar) / 10^4`, out of the collateral. A +/// close fee larger than the collateral refuses the open. +#[tokio::test(flavor = "multi_thread")] +async fn custody_fees_set_the_opening_fee() { + const SIZE: u64 = 1_000_000_000; + let graph = perps_graph(&[]).await; + let sol = custody_address(SOL_MINT); + let increase_bps = u64_at(graph.data(&sol), custody::INCREASE_POSITION_BPS) + 50; + // A scalar of 1,000 times the size charges 10 BPS of price impact. + let scalar = SIZE * 1_000; + let raised = json!({ + "increase_position_bps": increase_bps, "pricing.trade_impact_fee_scalar": scalar, + }); + let locker = perps_vm(&graph).await; + let raised = template_override(TRADING_FEES, sol, raised); + play(&locker, &graph, vec![raised]).await; + + let trader = Pubkey::new_unique(); + let (sent, at) = open_position(&locker, &graph, &SOL_LONG, trader, SIZE).await; + let opened = event(&sent.expect("the open failed"), "IncreasePositionEvent"); + let impact = SIZE * (SIZE as u128 * BPS).div_ceil(scalar as u128) as u64 / BPS as u64; + let fee = SIZE * increase_bps / BPS as u64 + impact; + assert_eq!(amount(&opened, "price_impact_fee_usd"), impact); + assert_eq!(amount(&opened, "position_fee_usd"), fee); + let collateral = u64_at(&account(&locker, &at).data, position::COLLATERAL_USD); + assert_eq!(collateral, amount(&opened, "collateral_usd_delta") - fee); + + let close_fee = json!({ "decrease_position_bps": 2_000 }); + play( + &locker, + &graph, + vec![template_override(TRADING_FEES, sol, close_fee)], + ) + .await; + let trader = Pubkey::new_unique(); + let (sent, _) = open_position(&locker, &graph, &SOL_LONG, trader, SIZE).await; + expect_error(sent, MAX_LEVERAGE, "a $200 close fee on $100"); +} + +fn usdc_deposit(locker: &SurfnetSvmLocker, graph: &PerpsGraph) -> Sent { + const DEPOSIT: u64 = 10_000_000; + let wallet = depositor(locker, USDC_MINT, DEPOSIT); + add_liquidity(locker, graph, wallet, USDC_MINT, DEPOSIT) +} + +/// Nothing publishes Doves prices on a surfnet. A copied feed refuses a keeper fill on a request +/// made after it (6045) and, 5 seconds on, a JLP deposit (6003), which reads every feed of the +/// pool. A `timestamp` the scenario writes ahead of the clock serves both, since Perps records the +/// clock and never the feed's time. +#[tokio::test(flavor = "multi_thread")] +async fn copied_prices_need_a_timestamp_the_scenario_writes() { + let graph = perps_graph(&[]).await; + let now = graph.clock.unix_timestamp; + let locker = perps_vm(&graph).await; + copy_feeds_at(&locker, &graph, now); + let trader = Pubkey::new_unique(); + let (requested, fill, _) = request_position(&locker, &graph, &SOL_LONG, trader, 1_000_000_000); + requested.expect("the request"); + set_clock(&locker, now + 5); + let refused = send(&locker, KEEPER, &[fill.clone()]); + expect_error( + refused, + PULL_ORACLE_PUBLISH_TIME_TOO_EARLY, + "a fill on the copied price", + ); + let stale = usdc_deposit(&locker, &graph); + expect_error( + stale, + STALE_ORACLE_PRICE, + "a deposit 5 seconds after the copy", + ); + + let ahead = |feed: &Pubkey| timestamp_override(*feed, now + DAY); + play(&locker, &graph, graph.feeds.iter().map(ahead).collect()).await; + send(&locker, KEEPER, &[fill]).expect("the fill on a price published after the request"); + set_clock(&locker, now + DAY - 1); + usdc_deposit(&locker, &graph).expect("a deposit a day on"); +} + +/// A USDC deposit that works on the live pool, refused by each lever that guards deposits. +#[tokio::test(flavor = "multi_thread")] +async fn pool_limits_and_switches_refuse_a_deposit() { + const DEPOSIT: u64 = 10_000_000_000; + let graph = perps_graph(&[]).await; + let sol_feed = graph.feed_of(custody_address(SOL_MINT)); + let aum = decode("Pool", &graph.data(&POOL)[8..])["aum_usd"].clone(); + let aum: u128 = aum.as_str().unwrap().parse().unwrap(); + let limits = |values: Value| template_override(POOL_LIMITS, POOL, values); + let cap = limits(json!({ "limit.max_aum_usd": (aum / 2).to_string() })); + let guard = limits(json!({ "max_lp_token_price_change_bps": 10_000 })); + let off = json!({ "permissions.allow_add_liquidity": false }); + let off = template_override(RISK, custody_address(USDC_MINT), off); + // SOL is about half the pool, so this moves the JLP price by about 10%. + let crashed = u64_at(graph.data(&sol_feed), feed::PRICE) * 8 / 10; + let crash = || price_override(sol_feed, crashed); + let lp_guard = Some(LP_TOKEN_PRICE_CHANGE_LIMIT_EXCEEDED); + let cases = [ + ("the live pool", vec![live_price(&graph)], None), + ( + "an AUM cap at half the pool", + vec![cap], + Some(POOL_AMOUNT_LIMIT), + ), + ( + "USDC deposits off", + vec![off], + Some(INSTRUCTION_NOT_ALLOWED), + ), + ("SOL down 20%", vec![crash()], lp_guard), + ( + "SOL down 20% with the guard raised", + vec![crash(), guard], + None, + ), + ]; + + for (what, overrides, refused) in cases { + let locker = perps_vm(&graph).await; + play(&locker, &graph, overrides).await; + let wallet = depositor(&locker, USDC_MINT, DEPOSIT); + let sent = add_liquidity(&locker, &graph, wallet, USDC_MINT, DEPOSIT); + match refused { + Some(code) => expect_error(sent, code, what), + None => { + sent.unwrap_or_else(|e| panic!("{what}: the deposit failed: {e:?}")); + assert!(token_amount(&locker, wallet.2) > 0, "{what}: no JLP minted"); + } + } + } +} + +/// With both weight taxes at 0, deposits and withdrawals pay `fees.add_remove_liquidity_bps`, +/// swaps with a non-stable token `fees.swap_bps x (1 + external_swap_fee_multiplier_bps / 10^4)` of +/// that token's custody, and stable swaps `fees.stable_swap_bps`. A real deposit pays what its +/// quote said. A weight tax moves deposit and stable swap fees off their base. +#[tokio::test(flavor = "multi_thread")] +async fn pool_fees_set_deposit_withdrawal_and_swap_fees() { + const DEPOSIT: u64 = 10_000_000_000; + const SWAP_USD: u128 = 100_000_000; + let graph = perps_graph(&[]).await; + let pool = decode("Pool", &graph.data(&POOL)[8..]); + let raised = |field: &str| amount(&pool["fees"], field) + 100; + let liquidity_bps = raised("add_remove_liquidity_bps"); + let (swap_bps, stable_bps) = (raised("swap_bps"), raised("stable_swap_bps")); + let sol = graph.data(&custody_address(SOL_MINT)); + let multiplier = BPS as u64 + u64_at(sol, custody::EXTERNAL_SWAP_FEE_MULTIPLIER_BPS); + let fees = |values: Value| vec![template_override(POOL_FEES, POOL, values)]; + let untaxed = json!({ + "fees.add_remove_liquidity_bps": liquidity_bps, + "fees.swap_bps": swap_bps, + "fees.stable_swap_bps": stable_bps, + "fees.tax_bps": 0, + "fees.stable_swap_tax_bps": 0, + }); + let locker = perps_vm(&graph).await; + play(&locker, &graph, fees(untaxed)).await; + + let named = [("custody", custody_address(USDC_MINT))]; + let params = json!({ "lp_amount_in": 10_000_000 }); + let name = "get_remove_liquidity_amount_and_fee2"; + let withdrawal = quote(&locker, &graph, name, &named, params).expect("withdrawal quote"); + assert_eq!(amount(&withdrawal, "fee_bps"), liquidity_bps, "withdrawal"); + let quoted = deposit_quote(&locker, &graph, USDC_MINT, DEPOSIT).expect("deposit quote"); + assert_eq!(amount("ed, "fee_bps"), liquidity_bps, "deposit"); + assert_eq!(amount("ed, "fee"), DEPOSIT * liquidity_bps / BPS as u64); + let wallet = depositor(&locker, USDC_MINT, DEPOSIT); + let deposited = add_liquidity(&locker, &graph, wallet, USDC_MINT, DEPOSIT); + let deposited = event(&deposited.expect("the deposit failed"), "AddLiquidityEvent"); + assert_eq!(amount(&deposited, "fee_bps"), liquidity_bps); + let kept = DEPOSIT - amount("ed, "fee"); + assert_eq!(amount(&deposited, "token_amount_after_fee"), kept); + let minted = token_amount(&locker, wallet.2); + assert_eq!(minted, amount("ed, "amount"), "JLP minted"); + let to_sol = swap_fee(&locker, &graph, USDC_MINT, SOL_MINT, SWAP_USD); + assert_eq!(to_sol, swap_bps * multiplier / BPS as u64, "USDC to SOL"); + let stable_swap = || swap_fee(&locker, &graph, USDC_MINT, JUPUSD_MINT, SWAP_USD); + assert_eq!(stable_swap(), stable_bps, "USDC to JupUSD"); + + let taxed = json!({ "fees.tax_bps": 1_000, "fees.stable_swap_tax_bps": 1_000 }); + play(&locker, &graph, fees(taxed)).await; + let taxed = [USDC_MINT, SOL_MINT, BTC_MINT].map(|mint| { + let deposit = graph.tokens_worth(mint, DEPOSIT as u128); + let quoted = deposit_quote(&locker, &graph, mint, deposit).expect("taxed quote"); + amount("ed, "fee_bps") + }); + let moved = taxed.iter().any(|bps| *bps != liquidity_bps); + assert!(moved, "taxed deposits: {taxed:?}"); + assert_ne!(stable_swap(), stable_bps, "a taxed USDC to JupUSD swap"); +} diff --git a/crates/core/src/tests/mod.rs b/crates/core/src/tests/mod.rs index f43607e64..ccf06ee8b 100644 --- a/crates/core/src/tests/mod.rs +++ b/crates/core/src/tests/mod.rs @@ -1,6 +1,8 @@ pub mod helpers; pub mod integration; #[cfg(feature = "integration-tests")] +pub mod jupiter_perps; +#[cfg(feature = "integration-tests")] pub mod kamino; #[cfg(feature = "integration-tests")] pub mod phoenix; diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index 3ca1257e8..30cd2a7d8 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -213,7 +213,9 @@ pub struct CallSurfnetRpcParams { A list of all the RPC methods available can be found at str:///rpc_endpoints" )] pub method: String, - #[schemars(description = "The parameters to pass to the RPC method")] + #[schemars( + description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"\"}}}}}]}}}]" + )] pub params: Vec, } From 7317ff7501924df0e16f532b0e404725da85a22d Mon Sep 17 00:00:00 2001 From: failfmi Date: Wed, 7 Oct 2026 13:43:14 +0300 Subject: [PATCH 02/11] ci: claude code review --- .github/workflows/claude-review.yml | 141 ++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/workflows/claude-review.yml diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 000000000..844c44d68 --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,141 @@ +name: Perform a code review when a pull request is created. +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + claude: + # Skip drafts, and skip fork PRs: on `pull_request` they don't get the + # ANTHROPIC_API_KEY secret, so the review would only fail. + if: >- + !github.event.pull_request.draft && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + structured_output: ${{ steps.run_claude.outputs.structured_output }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + # Explicitly check out the PR's merge commit. + ref: refs/pull/${{ github.event.pull_request.number }}/merge + persist-credentials: false + + - name: Pre-fetch base and head refs for the PR + env: + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Pass GitHub expressions through env and quote shell expansions. + git fetch --no-tags origin \ + "$PR_BASE_REF" \ + "+refs/pull/$PR_NUMBER/head" + + # If you want Claude to build and run code, install any dependencies that + # need to be downloaded before the "Run Claude" step, and add the commands + # it may run to --allowedTools below (e.g. "Bash(npm test:*)"). Any tool + # not listed there is denied. + + - name: Run Claude + id: run_claude + uses: anthropics/claude-code-action@86d88e619d8e6caf07b5c3944dd14f441c533718 # v1.0.243 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Use this job's read-only token instead of the Claude GitHub App, + # which needs `id-token: write` and would get write access. + github_token: ${{ github.token }} + prompt: | + This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}. + + Review ONLY the changes introduced by the PR, so consider: + git log --oneline ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} + git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} + + Suggest any improvements, potential bugs, or issues. + Be concise and specific in your feedback. + + Return your review in two parts: + - `summary`: a short Markdown overview of the PR and your overall assessment. + - `comments`: one entry per specific issue, anchored to the line it concerns. + `path` is relative to the repository root. `line` is the line number in + the PR head version of the file (${{ github.event.pull_request.head.sha }}), + and must be a line shown in the diff above. The working tree is the merge + commit, so its line numbers can differ; check them with + `git show ${{ github.event.pull_request.head.sha }}:`. + Put issues that don't belong to a single line in `summary` instead. + + Pull request title and body: + ---- + ${{ github.event.pull_request.title }} + ${{ github.event.pull_request.body }} + claude_args: | + --allowedTools "Read,Grep,Glob,Bash(git log:*),Bash(git diff:*),Bash(git show:*)" + --json-schema '{ + "type": "object", + "properties": { + "summary": { + "type": "string", + "description": "Overall review as Markdown, posted as the review body." + }, + "comments": { + "type": "array", + "description": "Inline comments, each anchored to one line of the PR diff.", + "items": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "File path relative to the repository root." }, + "line": { "type": "integer", "minimum": 1, "description": "Line number in the PR head version of the file. Must be within the diff." }, + "body": { "type": "string", "description": "The comment, as Markdown." } + }, + "required": ["path", "line", "body"] + } + } + }, + "required": ["summary", "comments"] + }' + + post_feedback: + runs-on: ubuntu-latest + needs: claude + if: needs.claude.outputs.structured_output != '' + permissions: + issues: write + pull-requests: write + steps: + - name: Report Claude feedback + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + CLAUDE_STRUCTURED_OUTPUT: ${{ needs.claude.outputs.structured_output }} + with: + github-token: ${{ github.token }} + script: | + const { summary = '', comments = [] } = JSON.parse(process.env.CLAUDE_STRUCTURED_OUTPUT); + const pr = context.payload.pull_request; + if (!summary.trim() && comments.length === 0) { + core.info('Claude returned an empty review; nothing to post.'); + return; + } + + try { + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + commit_id: pr.head.sha, + event: 'COMMENT', + body: summary.trim() || 'See inline comments.', + comments: comments.map(({ path, line, body }) => ({ path, line, side: 'RIGHT', body })), + }); + } catch (error) { + // GitHub rejects the whole review if any comment points outside the + // diff. Fall back to a single PR comment so no feedback is lost. + core.warning(`Could not post inline review (${error.message}); posting a plain comment instead.`); + const details = comments.map(({ path, line, body }) => `**\`${path}:${line}\`**\n\n${body}`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: [summary.trim(), ...details].filter(Boolean).join('\n\n---\n\n'), + }); + } From 2b09029151c0d7636ae54d60bd8fa5295b8c6c1e Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 3 Nov 2026 08:53:29 +0200 Subject: [PATCH 03/11] fix(scenarios): expose the Doves timestamp and document reloads --- .../scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml | 3 +++ crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml index e410c7ef0..8c8ca83c0 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -18,6 +18,9 @@ templates: - path: price label: Price description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8" + - path: timestamp + label: Publish time + description: "Unix seconds the price was published. Deposits, swaps and quotes refuse a feed 5 seconds older than the clock, and a keeper fill needs a time after its request; Perps accepts a time ahead of the clock" address: type: pubkey llm_context: | diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md index 882dec4d9..60f7e5aba 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md @@ -165,6 +165,7 @@ deposits then fail with 6043. | 6045 `PullOraclePublishTimeTooEarly` | a keeper filled a position request with a price published no later than the request | write the position's feed `timestamp` later than the request - see "Prices on a fork" | | 6066 `LpTokenPriceChangeLimitExceeded` | the JLP price moved past the guard | raise `max_lp_token_price_change_bps` | | 6000 `MathOverflow` on a stablecoin custody | its borrow-lend debt overflowed a large rate or time jump | lower the rate or the time step | +| an earlier change to a custody, the pool or a feed is gone | a later override with `fetchBeforeUse: true` loaded the account from mainnet again, which drops every local change to it | give later overrides of the same account `fetchBeforeUse: false` | # IDL provenance From 68c05178842a08cfb404a2d8acc448b1044254b9 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 3 Nov 2026 10:52:23 +0200 Subject: [PATCH 04/11] docs(scenarios): say when Perps overrides reload and how long quotes last --- .../jupiter-perps/doves/v1/overrides.yaml | 6 ++++-- .../protocols/jupiter-perps/v1/overrides.yaml | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml index 8c8ca83c0..218bcf665 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -20,7 +20,7 @@ templates: description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8" - path: timestamp label: Publish time - description: "Unix seconds the price was published. Deposits, swaps and quotes refuse a feed 5 seconds older than the clock, and a keeper fill needs a time after its request; Perps accepts a time ahead of the clock" + description: "Unix seconds the price was published. Deposits and swaps refuse a feed 5 seconds older than the clock (quotes 15), and a keeper fill needs a time after its request; Perps accepts a time ahead of the clock" address: type: pubkey llm_context: | @@ -37,7 +37,9 @@ templates: SET THE PRICE: - price = usd_price x 10^-expo; read expo from the feed and keep it. $80.00 -> 8000000000 - - Set fetchBeforeUse: true so the feed's other fields keep their live values. + - Set fetchBeforeUse: true so the feed's other fields keep their live values, and false + on a later override of the same feed in another scenario or slot: a reload drops the + earlier local change. - Set only the price for a liquidation: liquidations do not check a feed's age. - Nothing publishes Doves prices on a surfnet. A JLP deposit, withdrawal, swap or quote reads every custody's feed and refuses one older than 5 seconds (a quote 15), and a diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml index 6343ad6a8..e35fc69ae 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml @@ -58,7 +58,9 @@ templates: FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint - is the token's mint. Set fetchBeforeUse: true. + is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in + another scenario or slot, since a reload drops earlier local changes; a custody reloaded on + its own can also fall out of step with its vault (6014), which the README covers. pricing.max_leverage is the liquidation threshold, in BPS (10000 = 1x). A keeper liquidates a position once collateral + PnL - close and borrow fees falls below @@ -107,7 +109,10 @@ templates: FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint - is the token's mint. Set fetchBeforeUse: true. A long pays its own token's custody. A + is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in + another scenario or slot, since a reload drops earlier local changes; a custody reloaded on + its own can also fall out of step with its vault (6014), which the README covers. + A long pays its own token's custody. A short does not pay the traded token's rate; its fee follows the USDC or USDT custody holding its collateral. Those custodies also carry borrow-lend debt, and a high rate or a long time jump on them overflows (error 6000). @@ -141,7 +146,9 @@ templates: FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint - is the token's mint. Set fetchBeforeUse: true. + is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in + another scenario or slot, since a reload drops earlier local changes; a custody reloaded on + its own can also fall out of step with its vault (6014), which the README covers. Fees come out of the position's collateral, in USD with 6 decimals: - opening or increasing: size x increase_position_bps / 10000 @@ -177,7 +184,9 @@ templates: value: 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq llm_context: | Use this template to refuse or reopen JLP deposits. The address is the JLP Pool, the only - pool Jupiter Perps runs. Set fetchBeforeUse: true. limit.max_aum_usd and + pool Jupiter Perps runs. Set fetchBeforeUse: true on the first override of the pool and + false on later ones in another scenario or slot, since a reload drops earlier local changes. + limit.max_aum_usd and limit.token_weightage_buffer_bps are u128: pass them as decimal strings. - limit.max_aum_usd: a deposit that would take the pool's value above it is refused @@ -218,7 +227,8 @@ templates: value: 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq llm_context: | Use this template to change what JLP deposits, withdrawals and pool swaps cost. The - address is the JLP Pool, the only pool Jupiter Perps runs. Set fetchBeforeUse: true. + address is the JLP Pool, the only pool Jupiter Perps runs. Set fetchBeforeUse: true on the first override of the pool and + false on later ones in another scenario or slot, since a reload drops earlier local changes. Every field is in BPS (10000 = 100%). - fees.add_remove_liquidity_bps: base fee on JLP deposits and withdrawals. From cb3eb43817f5fe4e28791ff351cbce172f0bcf60 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 3 Nov 2026 13:20:12 +0200 Subject: [PATCH 05/11] docs(scenarios): rewrap the Perps custody notes and mark the collection mixed --- .../protocols/jupiter-perps/v1/overrides.yaml | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml index e35fc69ae..b56fdaf3c 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml @@ -1,6 +1,6 @@ protocol: jupiter-perps version: v1 -account_type: Custody +account_type: Multiple idl_file_path: idl.json tags: @@ -58,9 +58,9 @@ templates: FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint - is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in - another scenario or slot, since a reload drops earlier local changes; a custody reloaded on - its own can also fall out of step with its vault (6014), which the README covers. + is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on + later ones in another scenario or slot, since a reload drops earlier local changes; a custody + reloaded on its own can also fall out of step with its vault (6014), which the README covers. pricing.max_leverage is the liquidation threshold, in BPS (10000 = 1x). A keeper liquidates a position once collateral + PnL - close and borrow fees falls below @@ -109,13 +109,13 @@ templates: FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint - is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in - another scenario or slot, since a reload drops earlier local changes; a custody reloaded on - its own can also fall out of step with its vault (6014), which the README covers. - A long pays its own token's custody. A - short does not pay the traded token's rate; its fee follows the USDC or USDT custody - holding its collateral. Those custodies also carry borrow-lend debt, and a high rate or a - long time jump on them overflows (error 6000). + is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on + later ones in another scenario or slot, since a reload drops earlier local changes; a custody + reloaded on its own can also fall out of step with its vault (6014), which the README covers. + + A long pays its own token's custody. A short does not pay the traded token's rate; its fee + follows the USDC or USDT custody holding its collateral. Those custodies also carry + borrow-lend debt, and a high rate or a long time jump on them overflows (error 6000). EXAMPLE - 1000% a year on SOL: jump_rate_state.min_rate_bps: 100000 @@ -146,9 +146,9 @@ templates: FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint - is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in - another scenario or slot, since a reload drops earlier local changes; a custody reloaded on - its own can also fall out of step with its vault (6014), which the README covers. + is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on + later ones in another scenario or slot, since a reload drops earlier local changes; a custody + reloaded on its own can also fall out of step with its vault (6014), which the README covers. Fees come out of the position's collateral, in USD with 6 decimals: - opening or increasing: size x increase_position_bps / 10000 @@ -186,8 +186,7 @@ templates: Use this template to refuse or reopen JLP deposits. The address is the JLP Pool, the only pool Jupiter Perps runs. Set fetchBeforeUse: true on the first override of the pool and false on later ones in another scenario or slot, since a reload drops earlier local changes. - limit.max_aum_usd and - limit.token_weightage_buffer_bps are u128: pass them as decimal strings. + limit.max_aum_usd and limit.token_weightage_buffer_bps are u128: pass them as decimal strings. - limit.max_aum_usd: a deposit that would take the pool's value above it is refused (error 6024). Below the pool's aum_usd, every deposit is refused. @@ -227,9 +226,9 @@ templates: value: 5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq llm_context: | Use this template to change what JLP deposits, withdrawals and pool swaps cost. The - address is the JLP Pool, the only pool Jupiter Perps runs. Set fetchBeforeUse: true on the first override of the pool and - false on later ones in another scenario or slot, since a reload drops earlier local changes. - Every field is in BPS (10000 = 100%). + address is the JLP Pool, the only pool Jupiter Perps runs. Set fetchBeforeUse: true on the + first override of the pool and false on later ones in another scenario or slot, since a reload + drops earlier local changes. Every field is in BPS (10000 = 100%). - fees.add_remove_liquidity_bps: base fee on JLP deposits and withdrawals. - fees.swap_bps: base fee on swaps that involve a non-stable token. That token's custody From 7a2bad69f5235074e63dae80ca7f32c378acc666 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Mon, 9 Nov 2026 11:12:23 +0200 Subject: [PATCH 06/11] feat(scenarios): keep Doves price feeds published with keep-alive --- crates/core/src/scenarios/README.md | 2 +- .../jupiter-perps/doves/v1/keep-alive.yaml | 17 +++ .../jupiter-perps/doves/v1/overrides.yaml | 21 ++-- .../protocols/jupiter-perps/v1/README.md | 31 +++-- crates/core/src/scenarios/registry.rs | 23 ++++ crates/core/src/tests/jupiter_perps/mod.rs | 114 +++++++++++------- 6 files changed, 143 insertions(+), 65 deletions(-) create mode 100644 crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/keep-alive.yaml diff --git a/crates/core/src/scenarios/README.md b/crates/core/src/scenarios/README.md index a4ba1dd73..47dcebf29 100644 --- a/crates/core/src/scenarios/README.md +++ b/crates/core/src/scenarios/README.md @@ -21,7 +21,7 @@ Protocols that are natively supported by Surfpool will have their IDLs included - **Pump v1** - Bonding curve launchpad with curve reserve and global config override templates - **Phoenix Eternal** - Perpetuals venue with trader collateral, mark price, maintenance margin, market fee, withdraw limit, trader capability, stop-loss trigger and delegated permission templates. See [protocols/phoenix-eternal/README.md](./protocols/phoenix-eternal/README.md) - **PumpSwap v1** - Constant-product AMM with pool state and global config override templates, including canonical pool derivation for migrated pump.fun coins -- **Jupiter Perps** - Perpetuals exchange and JLP pool with oracle price, custody risk, borrow rate, trading fee, pool limit and pool fee templates, across the Perps and Doves programs. See [protocols/jupiter-perps/v1/README.md](./protocols/jupiter-perps/v1/README.md) +- **Jupiter Perps** - Perpetuals exchange and JLP pool with oracle price, custody risk, borrow rate, trading fee, pool limit and pool fee templates, across the Perps and Doves programs; its oracle prices are kept published. See [protocols/jupiter-perps/v1/README.md](./protocols/jupiter-perps/v1/README.md) For custom protocols, an IDL can be registered at runtime using the [`surfnet_registerIdl`](https://docs.surfpool.run/rpc/cheatcodes#surfnet-registeridl) RPC cheatcode. diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/keep-alive.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/keep-alive.yaml new file mode 100644 index 000000000..fba323f70 --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/keep-alive.yaml @@ -0,0 +1,17 @@ +protocol: jupiter-perps-doves +version: v1 +account_type: AgPriceFeed +idl_file_path: idl.json + +templates: + - id: jupiter-perps-doves-publication + name: Price publication + description: The Doves publisher republishing a price + idl_account_name: AgPriceFeed + address: + type: pubkey + properties: + - path: timestamp + description: >- + Unix seconds the price was published. Perps refuses a feed older than its staleness + window and a keeper fill on a price published no later than its request. diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml index 218bcf665..aa1c15c1a 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -9,6 +9,11 @@ tags: - perpetuals - defi +keep_alive: + - template: jupiter-perps-doves-publication + address: { type: override } + values: { timestamp: $unix_s } + templates: - id: jupiter-perps-doves-price name: Override Perps Oracle Price @@ -20,7 +25,7 @@ templates: description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8" - path: timestamp label: Publish time - description: "Unix seconds the price was published. Deposits and swaps refuse a feed 5 seconds older than the clock (quotes 15), and a keeper fill needs a time after its request; Perps accepts a time ahead of the clock" + description: Unix seconds the price was published. Keep-alive writes the Clock time every slot address: type: pubkey llm_context: | @@ -40,12 +45,14 @@ templates: - Set fetchBeforeUse: true so the feed's other fields keep their live values, and false on a later override of the same feed in another scenario or slot: a reload drops the earlier local change. - - Set only the price for a liquidation: liquidations do not check a feed's age. - - Nothing publishes Doves prices on a surfnet. A JLP deposit, withdrawal, swap or quote - reads every custody's feed and refuses one older than 5 seconds (a quote 15), and a - keeper fill needs a price published after its request. For those, also write timestamp - on every feed the action reads, ahead of the Surfnet clock by as long as the test runs: - Perps accepts a time ahead of the clock and records only the clock. + + THE PRICE STAYS PUBLISHED BY ITSELF. From the slot this override plays, Surfpool republishes + this feed every slot at the Surfnet clock's time, as the Doves publisher does on mainnet, so + the price never goes stale and a keeper fill finds it published after its request. Only the + feeds an override names are republished. A JLP deposit, withdrawal, swap or quote reads every + custody's feed and refuses a stale one, so write this template on every feed of the pool for + those, at its live price where the scenario does not move it. A position reads the feeds of + its market and collateral custodies. LIQUIDATIONS: a keeper liquidates a position once collateral + PnL - close and borrow fees falls below size x 10000 / the custody's pricing.max_leverage. Move a long's price diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md index 60f7e5aba..c395205ef 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md @@ -8,7 +8,8 @@ Templates for the Jupiter Perps exchange and the JLP pool behind it, across two | Doves oracle | `DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e` | `jupiter-perps-doves` (`doves/v1`) | Perps reads every price from a Doves `AgPriceFeed`. It never reads the custody's Pyth account, so -`pyth-price-feed-v2` does not move Perps prices. +`pyth-price-feed-v2` does not move Perps prices. Surfpool keeps a feed published on its own once a +scenario overrides it. # Template index @@ -59,21 +60,19 @@ old (5 seconds), a quote once it is three times that, at most 15, and a keeper f price was published after the position request. JLP deposits, withdrawals, swaps and quotes read every custody's feed; positions read their own feeds; liquidations do not check the age. -## Prices on a fork +## Keep-alive (Surfpool handles it) -Nothing publishes Doves prices on a surfnet, so a copied feed ages with the Surfnet clock and a -keeper fill finds no price published after its request. For deposits, swaps, quotes and positions, -write `timestamp` on every feed the action reads, ahead of the clock by as long as the test runs. -Perps accepts a time ahead of the clock and records only the clock, so the outcome is the same as -with a fresh price: +On mainnet the Doves publisher republishes every feed's price within seconds. Nobody runs it +against a surfnet, so Surfpool republishes each feed a `jupiter-perps-doves-price` override names +every slot ([keep-alive](../../../README.md#keep-alive)): its `timestamp` follows the Surfnet clock +and its price stays the scenario's. The feed never goes stale, and a keeper fill finds a price +published after its request. Keep-alive starts in the slot the override plays and runs until the +network is reset. -``` -template: jupiter-perps-doves-price address: each feed the action reads -timestamp: -``` - -Keep-alive ([solana-foundation/surfpool#906](https://github.com/solana-foundation/surfpool/pull/906)) -will keep the feeds published once it lands. +A feed no override names is not republished and goes stale. A JLP deposit, withdrawal, swap or +quote reads every custody's feed, so write `jupiter-perps-doves-price` on each of them for those, +at its live price where the scenario does not move it. A position reads the feeds of its market +and collateral custodies. ## Make a position liquidatable @@ -154,7 +153,7 @@ deposits then fail with 6043. | Error | Meaning | Fix | |---|---|---| -| 6003 `StaleOraclePrice` | a feed the instruction read is too old: 5 seconds for a deposit, 15 for a quote | write `timestamp` ahead of the clock on every feed of the pool - see "Prices on a fork" | +| 6003 `StaleOraclePrice` | a feed the instruction read is too old: 5 seconds for a deposit, 15 for a quote | expected on a feed no override names: write `jupiter-perps-doves-price` on every feed of the pool - see "Keep-alive" | | 6009 `InvalidPositionState` | the position is not liquidatable | move the price further, or lower `pricing.max_leverage` | | 6014 `InvalidCustodyBalance` | a custody's vault and staked lamports hold less than its `assets.owned`: the two were copied from mainnet at different moments, as when Studio's market list or a lookup read the custody first, or `fetchBeforeUse` reloaded only the custody | `surfnet_resetAccount` on the custody and on its `token_account`; the next transaction loads them together | | 6022 `MaxLeverage` | the position would open past its leverage limit, as when its close fee exceeds its collateral | lower `decrease_position_bps` or add collateral | @@ -162,7 +161,7 @@ deposits then fail with 6043. | 6027 `InstructionNotAllowed` | a `permissions` switch is off | set it back to `true` | | 6034 `InvalidKeeper` | the liquidator is not the keeper the program accepts | sign as the keeper | | 6043 `ExceedTokenWeightage` | the deposit leaves the token too far above its target weight | raise `limit.token_weightage_buffer_bps` | -| 6045 `PullOraclePublishTimeTooEarly` | a keeper filled a position request with a price published no later than the request | write the position's feed `timestamp` later than the request - see "Prices on a fork" | +| 6045 `PullOraclePublishTimeTooEarly` | a keeper filled a position request with a price published no later than the request | override the position's feed, and fill in a later second than the request - see "Keep-alive" | | 6066 `LpTokenPriceChangeLimitExceeded` | the JLP price moved past the guard | raise `max_lp_token_price_change_bps` | | 6000 `MathOverflow` on a stablecoin custody | its borrow-lend debt overflowed a large rate or time jump | lower the rate or the time step | | an earlier change to a custody, the pool or a feed is gone | a later override with `fetchBeforeUse: true` loaded the account from mainnet again, which drops every local change to it | give later overrides of the same account `fetchBeforeUse: false` | diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index 3dd490602..740c4e760 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -79,6 +79,8 @@ pub const JUPITER_PERPS_DOVES_IDL_CONTENT: &str = include_str!("./protocols/jupiter-perps/doves/v1/idl.json"); pub const JUPITER_PERPS_DOVES_OVERRIDES_CONTENT: &str = include_str!("./protocols/jupiter-perps/doves/v1/overrides.yaml"); +pub const JUPITER_PERPS_DOVES_KEEP_ALIVE_CONTENT: &str = + include_str!("./protocols/jupiter-perps/doves/v1/keep-alive.yaml"); /// Registry for managing override templates loaded from YAML files #[derive(Clone, Debug, Default)] @@ -351,6 +353,12 @@ impl TemplateRegistry { JUPITER_PERPS_DOVES_OVERRIDES_CONTENT, "jupiter-perps-doves", ); + let idl = serde_json::from_str(JUPITER_PERPS_DOVES_IDL_CONTENT).expect("doves idl"); + self.load_keep_alive( + Some(idl), + JUPITER_PERPS_DOVES_KEEP_ALIVE_CONTENT, + "jupiter-perps-doves", + ); } fn load_protocol_overrides( @@ -1094,6 +1102,21 @@ templates: TemplateRegistry::new().check_keep_alive_imports().unwrap(); } + #[test] + fn doves_price_keeps_its_feed_published() { + let registry = TemplateRegistry::new(); + let template = registry.get("jupiter-perps-doves-price").unwrap(); + let [import] = template.keep_alive.as_slice() else { + panic!("jupiter-perps-doves-price must keep its feed published"); + }; + assert_eq!(import.template, "jupiter-perps-doves-publication"); + assert_eq!(import.address, KeepAliveAddress::Override); + assert_eq!( + import.values, + HashMap::from([("timestamp".to_string(), serde_json::json!("$unix_s"))]) + ); + } + #[test] fn raw_layout_collection_rejects_invalid_write_definitions_at_load_time() { fn rejected(yaml: &str, expected: &str) { diff --git a/crates/core/src/tests/jupiter_perps/mod.rs b/crates/core/src/tests/jupiter_perps/mod.rs index 0b2ff9c4a..aac6fddb9 100644 --- a/crates/core/src/tests/jupiter_perps/mod.rs +++ b/crates/core/src/tests/jupiter_perps/mod.rs @@ -387,7 +387,8 @@ fn local_vm(graph: &PerpsGraph) -> SurfnetSvmLocker { let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); let locker = SurfnetSvmLocker::new(svm); locker.with_svm_writer(|svm| { - svm.inner.set_sysvar(&graph.clock); + // A time travel, not a Clock write: keep-alive stamps the time the surfnet keeps. + svm.apply_time_travel(graph.clock.clone()).unwrap(); for (address, account) in &graph.accounts { svm.set_account(address, account.clone()).unwrap(); } @@ -417,10 +418,14 @@ fn fund(locker: &SurfnetSvmLocker, address: Pubkey) { set_account(locker, address, account); } -fn set_clock(locker: &SurfnetSvmLocker, unix_timestamp: i64) { - let mut clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); - clock.unix_timestamp = unix_timestamp; - locker.with_svm_writer(|svm| svm.inner.set_sysvar(&clock)); +/// A time travel to `unix_timestamp` in the open slot, which keep-alive republishes at. +async fn travel(locker: &SurfnetSvmLocker, unix_timestamp: i64) { + let clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); + let clock = Clock { + unix_timestamp, + ..clock + }; + locker.apply_time_travel(clock, &None).await.unwrap(); } fn local(locker: &SurfnetSvmLocker, address: &Pubkey) -> Option { @@ -451,14 +456,14 @@ fn parsed(locker: &SurfnetSvmLocker, address: Pubkey) -> Value { parsed.parsed } -/// Plays the overrides through the scenario materializer at the graph's slot, then reads each +/// Plays the overrides through the scenario materializer at the next slot, then reads each /// pubkey-addressed target back as `jsonParsed` and checks the last value given for each field. -async fn play(locker: &SurfnetSvmLocker, graph: &PerpsGraph, overrides: Vec) { +async fn play(locker: &SurfnetSvmLocker, overrides: Vec) { let mut scenario = Scenario::new("Jupiter Perps".into(), "State a test prepares".into()); for instance in &overrides { scenario.add_override(instance.clone()); } - let slot = graph.clock.slot; + let slot = locker.get_latest_absolute_slot() + 1; locker.register_scenario(scenario, Some(slot)).unwrap(); let materialized = locker.materialize_overrides_for_slot(&None, slot); materialized.await.unwrap(); @@ -495,10 +500,6 @@ fn live_price(graph: &PerpsGraph) -> OverrideInstance { price_override(feed, u64_at(graph.data(&feed), feed::PRICE)) } -fn timestamp_override(feed: Pubkey, timestamp: i64) -> OverrideInstance { - template_override(PRICE, feed, json!({ "timestamp": timestamp })) -} - fn pyth_override() -> OverrideInstance { let registry = TemplateRegistry::new(); let template = registry.get("pyth-price-feed-v2").unwrap().clone(); @@ -786,10 +787,10 @@ async fn stressed( what: &str, ) -> SurfnetSvmLocker { let locker = perps_vm(graph).await; - play(&locker, graph, healthy).await; + play(&locker, healthy).await; let refused = liquidate(&locker, graph, KEEPER, at); expect_error(refused, INVALID_POSITION_STATE, what); - play(&locker, graph, stress).await; + play(&locker, stress).await; locker } @@ -963,8 +964,9 @@ fn request_position( } /// Opens a position as the app does: the request, then the keeper's fill one second later, since -/// a request cannot run in the second it was made, on a price the scenario publishes after the -/// request. Returns the first failure or the keeper's transaction, and the position. +/// a request cannot run in the second it was made, on a price published after the request. The +/// scenario names the market's feed at its current price, so keep-alive republishes it. Returns +/// the first failure or the keeper's transaction, and the position. async fn open_position( locker: &SurfnetSvmLocker, graph: &PerpsGraph, @@ -972,19 +974,15 @@ async fn open_position( trader: Pubkey, size_usd: u64, ) -> (Sent, Pubkey) { + let feed = graph.feed_of(custody_address(market.mint)); + let price = u64_at(&account(locker, &feed).data, feed::PRICE); + play(locker, vec![price_override(feed, price)]).await; let (requested, fill, at) = request_position(locker, graph, market, trader, size_usd); if requested.is_err() { return (requested, at); } let clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); - set_clock(locker, clock.unix_timestamp + 1); - let feed = graph.feed_of(custody_address(market.mint)); - play( - locker, - graph, - vec![timestamp_override(feed, clock.unix_timestamp + 1)], - ) - .await; + travel(locker, clock.unix_timestamp + 1).await; (send(locker, KEEPER, &[fill]), at) } @@ -1069,7 +1067,7 @@ async fn doves_price_moves_the_jlp_quote_and_pyth_does_not() { let feed = graph.feed_of(sol); let pyth = pubkey_at(graph.data(&sol), custody::ORACLE_ACCOUNT); let locker = perps_vm(&graph).await; - play(&locker, &graph, vec![live_price(&graph)]).await; + play(&locker, vec![live_price(&graph)]).await; let quote_with = |pythnet: Pubkey| { let named = [("custody", sol), ("custody_pythnet_price_account", pythnet)]; let params = json!({ "token_amount_in": ONE_SOL }); @@ -1078,7 +1076,7 @@ async fn doves_price_moves_the_jlp_quote_and_pyth_does_not() { }; let baseline = quote_with(feed); - play(&locker, &graph, vec![pyth_override()]).await; + play(&locker, vec![pyth_override()]).await; let pyth_price = &parsed(&locker, pyth)["price_message"]["price"]; assert_eq!(pyth_price, 1_000_000, "the Pyth override missed"); for pythnet in [feed, pyth] { @@ -1086,7 +1084,7 @@ async fn doves_price_moves_the_jlp_quote_and_pyth_does_not() { } let shocked = u64_at(graph.data(&feed), feed::PRICE) * 8 / 10; - play(&locker, &graph, vec![price_override(feed, shocked)]).await; + play(&locker, vec![price_override(feed, shocked)]).await; let quoted = quote_with(feed); assert!( amount("ed, "amount") < amount(&baseline, "amount"), @@ -1176,7 +1174,7 @@ async fn custody_fees_set_the_opening_fee() { }); let locker = perps_vm(&graph).await; let raised = template_override(TRADING_FEES, sol, raised); - play(&locker, &graph, vec![raised]).await; + play(&locker, vec![raised]).await; let trader = Pubkey::new_unique(); let (sent, at) = open_position(&locker, &graph, &SOL_LONG, trader, SIZE).await; @@ -1191,7 +1189,6 @@ async fn custody_fees_set_the_opening_fee() { let close_fee = json!({ "decrease_position_bps": 2_000 }); play( &locker, - &graph, vec![template_override(TRADING_FEES, sol, close_fee)], ) .await; @@ -1206,12 +1203,12 @@ fn usdc_deposit(locker: &SurfnetSvmLocker, graph: &PerpsGraph) -> Sent { add_liquidity(locker, graph, wallet, USDC_MINT, DEPOSIT) } -/// Nothing publishes Doves prices on a surfnet. A copied feed refuses a keeper fill on a request -/// made after it (6045) and, 5 seconds on, a JLP deposit (6003), which reads every feed of the -/// pool. A `timestamp` the scenario writes ahead of the clock serves both, since Perps records the -/// clock and never the feed's time. +/// Only keep-alive publishes Doves prices on a surfnet. A copied feed refuses a keeper fill on a +/// request made after it (6045) and, 5 seconds on, a JLP deposit (6003), which reads every feed of +/// the pool. A price override on each feed republishes it every slot at the Surfnet clock's time +/// and the scenario's price, through produced slots and a time travel. #[tokio::test(flavor = "multi_thread")] -async fn copied_prices_need_a_timestamp_the_scenario_writes() { +async fn price_overrides_keep_their_feeds_published_at_the_clock() { let graph = perps_graph(&[]).await; let now = graph.clock.unix_timestamp; let locker = perps_vm(&graph).await; @@ -1219,7 +1216,7 @@ async fn copied_prices_need_a_timestamp_the_scenario_writes() { let trader = Pubkey::new_unique(); let (requested, fill, _) = request_position(&locker, &graph, &SOL_LONG, trader, 1_000_000_000); requested.expect("the request"); - set_clock(&locker, now + 5); + travel(&locker, now + 5).await; let refused = send(&locker, KEEPER, &[fill.clone()]); expect_error( refused, @@ -1233,10 +1230,45 @@ async fn copied_prices_need_a_timestamp_the_scenario_writes() { "a deposit 5 seconds after the copy", ); - let ahead = |feed: &Pubkey| timestamp_override(*feed, now + DAY); - play(&locker, &graph, graph.feeds.iter().map(ahead).collect()).await; + // SOL 1% lower moves JLP well inside its 5% guard; the other feeds keep their live price. + let sol_feed = graph.feed_of(custody_address(SOL_MINT)); + let prices: Vec<(Pubkey, u64)> = graph + .feeds + .iter() + .map(|feed| { + let live = u64_at(graph.data(feed), feed::PRICE); + let price = if *feed == sol_feed { + live * 99 / 100 + } else { + live + }; + (*feed, price) + }) + .collect(); + let overrides = prices + .iter() + .map(|(feed, price)| price_override(*feed, *price)); + play(&locker, overrides.collect()).await; + let published = |what: &str| { + let clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); + for (feed, price) in &prices { + let data = account(&locker, feed).data; + let timestamp = &data[feed::TIMESTAMP..feed::TIMESTAMP + 8]; + let timestamp = i64::from_le_bytes(timestamp.try_into().unwrap()); + assert_eq!(timestamp, clock.unix_timestamp, "{what}: {feed} timestamp"); + assert_eq!(u64_at(&data, feed::PRICE), *price, "{what}: {feed} price"); + } + }; + published("the slot the prices play"); send(&locker, KEEPER, &[fill]).expect("the fill on a price published after the request"); - set_clock(&locker, now + DAY - 1); + usdc_deposit(&locker, &graph).expect("a deposit on published prices"); + + for slot in 1..=3 { + locker.confirm_current_block(&None).await.unwrap(); + published(&format!("{slot} slots on")); + } + travel(&locker, now + DAY).await; + published("a time travel a day on"); usdc_deposit(&locker, &graph).expect("a deposit a day on"); } @@ -1279,7 +1311,7 @@ async fn pool_limits_and_switches_refuse_a_deposit() { for (what, overrides, refused) in cases { let locker = perps_vm(&graph).await; - play(&locker, &graph, overrides).await; + play(&locker, overrides).await; let wallet = depositor(&locker, USDC_MINT, DEPOSIT); let sent = add_liquidity(&locker, &graph, wallet, USDC_MINT, DEPOSIT); match refused { @@ -1316,7 +1348,7 @@ async fn pool_fees_set_deposit_withdrawal_and_swap_fees() { "fees.stable_swap_tax_bps": 0, }); let locker = perps_vm(&graph).await; - play(&locker, &graph, fees(untaxed)).await; + play(&locker, fees(untaxed)).await; let named = [("custody", custody_address(USDC_MINT))]; let params = json!({ "lp_amount_in": 10_000_000 }); @@ -1340,7 +1372,7 @@ async fn pool_fees_set_deposit_withdrawal_and_swap_fees() { assert_eq!(stable_swap(), stable_bps, "USDC to JupUSD"); let taxed = json!({ "fees.tax_bps": 1_000, "fees.stable_swap_tax_bps": 1_000 }); - play(&locker, &graph, fees(taxed)).await; + play(&locker, fees(taxed)).await; let taxed = [USDC_MINT, SOL_MINT, BTC_MINT].map(|mint| { let deposit = graph.tokens_worth(mint, DEPOSIT as u128); let quoted = deposit_quote(&locker, &graph, mint, deposit).expect("taxed quote"); From 3bcc64dfa9375f4ac7844f070a9fd364e31567b7 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Mon, 9 Nov 2026 11:12:23 +0200 Subject: [PATCH 07/11] test(scenarios): follow the Perps redeploy and check both stable swap directions --- crates/core/src/tests/jupiter_perps/mod.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/core/src/tests/jupiter_perps/mod.rs b/crates/core/src/tests/jupiter_perps/mod.rs index aac6fddb9..d2edad6a4 100644 --- a/crates/core/src/tests/jupiter_perps/mod.rs +++ b/crates/core/src/tests/jupiter_perps/mod.rs @@ -55,7 +55,7 @@ const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; const PERPS: Pubkey = Pubkey::from_str_const("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); const PERPS_PROGRAMDATA: Pubkey = Pubkey::from_str_const("C258LjtN8Q928yz2nJe5kwQycckGfWBSrhZBP2mTpLQt"); -const PERPS_DEPLOYED_SLOT: u64 = 413092478; +const PERPS_DEPLOYED_SLOT: u64 = 465131236; const DOVES: Pubkey = Pubkey::from_str_const("DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e"); const DOVES_PROGRAMDATA: Pubkey = Pubkey::from_str_const("4QRRLV8zvwvWDgPNWYXffcMLTS4M6UFCuj2pPMA1tg5D"); @@ -1368,8 +1368,11 @@ async fn pool_fees_set_deposit_withdrawal_and_swap_fees() { assert_eq!(minted, amount("ed, "amount"), "JLP minted"); let to_sol = swap_fee(&locker, &graph, USDC_MINT, SOL_MINT, SWAP_USD); assert_eq!(to_sol, swap_bps * multiplier / BPS as u64, "USDC to SOL"); - let stable_swap = || swap_fee(&locker, &graph, USDC_MINT, JUPUSD_MINT, SWAP_USD); - assert_eq!(stable_swap(), stable_bps, "USDC to JupUSD"); + let stable_swaps = || { + [(USDC_MINT, JUPUSD_MINT), (JUPUSD_MINT, USDC_MINT)] + .map(|(from, to)| swap_fee(&locker, &graph, from, to, SWAP_USD)) + }; + assert_eq!(stable_swaps(), [stable_bps; 2], "USDC and JupUSD swaps"); let taxed = json!({ "fees.tax_bps": 1_000, "fees.stable_swap_tax_bps": 1_000 }); play(&locker, fees(taxed)).await; @@ -1380,5 +1383,7 @@ async fn pool_fees_set_deposit_withdrawal_and_swap_fees() { }); let moved = taxed.iter().any(|bps| *bps != liquidity_bps); assert!(moved, "taxed deposits: {taxed:?}"); - assert_ne!(stable_swap(), stable_bps, "a taxed USDC to JupUSD swap"); + let swapped = stable_swaps(); + let moved = swapped.iter().any(|bps| *bps != stable_bps); + assert!(moved, "taxed USDC and JupUSD swaps: {swapped:?}"); } From 26cc9054616df9acab30681cbde4cae3c11996ae Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Mon, 9 Nov 2026 17:37:01 +0200 Subject: [PATCH 08/11] feat(scenarios): keep every loaded Doves feed published with owned keep-alive --- .../jupiter-perps/doves/v1/overrides.yaml | 15 +++-- .../protocols/jupiter-perps/v1/README.md | 21 +++---- crates/core/src/scenarios/registry.rs | 8 ++- crates/core/src/tests/jupiter_perps/mod.rs | 63 ++++++++++++------- 4 files changed, 66 insertions(+), 41 deletions(-) diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml index aa1c15c1a..47b98ca95 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -11,7 +11,7 @@ tags: keep_alive: - template: jupiter-perps-doves-publication - address: { type: override } + address: { type: owned, owner: DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e, account: AgPriceFeed } values: { timestamp: $unix_s } templates: @@ -46,13 +46,12 @@ templates: on a later override of the same feed in another scenario or slot: a reload drops the earlier local change. - THE PRICE STAYS PUBLISHED BY ITSELF. From the slot this override plays, Surfpool republishes - this feed every slot at the Surfnet clock's time, as the Doves publisher does on mainnet, so - the price never goes stale and a keeper fill finds it published after its request. Only the - feeds an override names are republished. A JLP deposit, withdrawal, swap or quote reads every - custody's feed and refuses a stale one, so write this template on every feed of the pool for - those, at its live price where the scenario does not move it. A position reads the feeds of - its market and collateral custodies. + THE PRICE STAYS PUBLISHED BY ITSELF. From the slot an override of this template plays, + Surfpool republishes every Doves feed the surfnet has loaded every slot at the Surfnet clock's + time, as the Doves publisher does on mainnet, including feeds a transaction loads later. A + JLP deposit, withdrawal, swap, quote or position finds every custody's feed fresh, and a + keeper fill finds the price published after its request. To keep one feed stale on purpose, + override it with keepAlive: false. LIQUIDATIONS: a keeper liquidates a position once collateral + PnL - close and borrow fees falls below size x 10000 / the custody's pricing.max_leverage. Move a long's price diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md index c395205ef..cc4d55b64 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md @@ -63,16 +63,15 @@ every custody's feed; positions read their own feeds; liquidations do not check ## Keep-alive (Surfpool handles it) On mainnet the Doves publisher republishes every feed's price within seconds. Nobody runs it -against a surfnet, so Surfpool republishes each feed a `jupiter-perps-doves-price` override names -every slot ([keep-alive](../../../README.md#keep-alive)): its `timestamp` follows the Surfnet clock -and its price stays the scenario's. The feed never goes stale, and a keeper fill finds a price -published after its request. Keep-alive starts in the slot the override plays and runs until the -network is reset. +against a surfnet, so from the slot a `jupiter-perps-doves-price` override plays, Surfpool +republishes every Doves feed the surfnet has loaded every slot +([keep-alive](../../../README.md#keep-alive)), including feeds a transaction loads later: each +`timestamp` follows the Surfnet clock and each price stays the scenario's. A JLP deposit, +withdrawal, swap or quote, or a position, finds every custody's feed fresh, and a keeper fill finds +a price published after its request. Keep-alive runs until the network is reset. -A feed no override names is not republished and goes stale. A JLP deposit, withdrawal, swap or -quote reads every custody's feed, so write `jupiter-perps-doves-price` on each of them for those, -at its live price where the scenario does not move it. A position reads the feeds of its market -and collateral custodies. +To keep one feed stale on purpose, override it with `keepAlive: false`; a later override of it with +`keepAlive: true` republishes it again. ## Make a position liquidatable @@ -153,7 +152,7 @@ deposits then fail with 6043. | Error | Meaning | Fix | |---|---|---| -| 6003 `StaleOraclePrice` | a feed the instruction read is too old: 5 seconds for a deposit, 15 for a quote | expected on a feed no override names: write `jupiter-perps-doves-price` on every feed of the pool - see "Keep-alive" | +| 6003 `StaleOraclePrice` | a feed the instruction read is too old: 5 seconds for a deposit, 15 for a quote | expected before any `jupiter-perps-doves-price` override plays, or on a feed overridden with `keepAlive: false`: override one feed, at its live price if the scenario does not move it - see "Keep-alive" | | 6009 `InvalidPositionState` | the position is not liquidatable | move the price further, or lower `pricing.max_leverage` | | 6014 `InvalidCustodyBalance` | a custody's vault and staked lamports hold less than its `assets.owned`: the two were copied from mainnet at different moments, as when Studio's market list or a lookup read the custody first, or `fetchBeforeUse` reloaded only the custody | `surfnet_resetAccount` on the custody and on its `token_account`; the next transaction loads them together | | 6022 `MaxLeverage` | the position would open past its leverage limit, as when its close fee exceeds its collateral | lower `decrease_position_bps` or add collateral | @@ -161,7 +160,7 @@ deposits then fail with 6043. | 6027 `InstructionNotAllowed` | a `permissions` switch is off | set it back to `true` | | 6034 `InvalidKeeper` | the liquidator is not the keeper the program accepts | sign as the keeper | | 6043 `ExceedTokenWeightage` | the deposit leaves the token too far above its target weight | raise `limit.token_weightage_buffer_bps` | -| 6045 `PullOraclePublishTimeTooEarly` | a keeper filled a position request with a price published no later than the request | override the position's feed, and fill in a later second than the request - see "Keep-alive" | +| 6045 `PullOraclePublishTimeTooEarly` | a keeper filled a position request with a price published no later than the request | override a feed before the fill, and fill in a later second than the request - see "Keep-alive" | | 6066 `LpTokenPriceChangeLimitExceeded` | the JLP price moved past the guard | raise `max_lp_token_price_change_bps` | | 6000 `MathOverflow` on a stablecoin custody | its borrow-lend debt overflowed a large rate or time jump | lower the rate or the time step | | an earlier change to a custody, the pool or a feed is gone | a later override with `fetchBeforeUse: true` loaded the account from mainnet again, which drops every local change to it | give later overrides of the same account `fetchBeforeUse: false` | diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index bca705709..6a1323b74 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -1142,7 +1142,13 @@ templates: panic!("jupiter-perps-doves-price must keep its feed published"); }; assert_eq!(import.template, "jupiter-perps-doves-publication"); - assert_eq!(import.address, KeepAliveAddress::Override); + assert_eq!( + import.address, + KeepAliveAddress::Owned { + owners: vec!["DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e".to_string()], + account: "AgPriceFeed".to_string(), + } + ); assert_eq!( import.values, HashMap::from([("timestamp".to_string(), serde_json::json!("$unix_s"))]) diff --git a/crates/core/src/tests/jupiter_perps/mod.rs b/crates/core/src/tests/jupiter_perps/mod.rs index d2edad6a4..a7b58e1fb 100644 --- a/crates/core/src/tests/jupiter_perps/mod.rs +++ b/crates/core/src/tests/jupiter_perps/mod.rs @@ -1205,8 +1205,9 @@ fn usdc_deposit(locker: &SurfnetSvmLocker, graph: &PerpsGraph) -> Sent { /// Only keep-alive publishes Doves prices on a surfnet. A copied feed refuses a keeper fill on a /// request made after it (6045) and, 5 seconds on, a JLP deposit (6003), which reads every feed of -/// the pool. A price override on each feed republishes it every slot at the Surfnet clock's time -/// and the scenario's price, through produced slots and a time travel. +/// the pool. A price override on one feed republishes every loaded feed every slot at the Surfnet +/// clock's time and its own price, through produced slots and a time travel, until an override +/// with keep-alive off lets its feed age. #[tokio::test(flavor = "multi_thread")] async fn price_overrides_keep_their_feeds_published_at_the_clock() { let graph = perps_graph(&[]).await; @@ -1230,46 +1231,66 @@ async fn price_overrides_keep_their_feeds_published_at_the_clock() { "a deposit 5 seconds after the copy", ); - // SOL 1% lower moves JLP well inside its 5% guard; the other feeds keep their live price. + // SOL 1% lower moves JLP well inside its 5% guard; no override names the other feeds, which + // keep their live price. let sol_feed = graph.feed_of(custody_address(SOL_MINT)); + let sol_price = u64_at(graph.data(&sol_feed), feed::PRICE) * 99 / 100; let prices: Vec<(Pubkey, u64)> = graph .feeds .iter() .map(|feed| { let live = u64_at(graph.data(feed), feed::PRICE); - let price = if *feed == sol_feed { - live * 99 / 100 - } else { - live - }; - (*feed, price) + (*feed, if *feed == sol_feed { sol_price } else { live }) }) .collect(); - let overrides = prices - .iter() - .map(|(feed, price)| price_override(*feed, *price)); - play(&locker, overrides.collect()).await; - let published = |what: &str| { + play(&locker, vec![price_override(sol_feed, sol_price)]).await; + let timestamp = |feed: &Pubkey| { + let data = account(&locker, feed).data; + i64::from_le_bytes( + data[feed::TIMESTAMP..feed::TIMESTAMP + 8] + .try_into() + .unwrap(), + ) + }; + let published = |what: &str, prices: &[(Pubkey, u64)]| { let clock: Clock = locker.with_svm_reader(|svm| svm.inner.get_sysvar()); - for (feed, price) in &prices { + for (feed, price) in prices { let data = account(&locker, feed).data; - let timestamp = &data[feed::TIMESTAMP..feed::TIMESTAMP + 8]; - let timestamp = i64::from_le_bytes(timestamp.try_into().unwrap()); - assert_eq!(timestamp, clock.unix_timestamp, "{what}: {feed} timestamp"); + assert_eq!( + timestamp(feed), + clock.unix_timestamp, + "{what}: {feed} timestamp" + ); assert_eq!(u64_at(&data, feed::PRICE), *price, "{what}: {feed} price"); } }; - published("the slot the prices play"); + published("the slot the SOL price plays", &prices); send(&locker, KEEPER, &[fill]).expect("the fill on a price published after the request"); usdc_deposit(&locker, &graph).expect("a deposit on published prices"); for slot in 1..=3 { locker.confirm_current_block(&None).await.unwrap(); - published(&format!("{slot} slots on")); + published(&format!("{slot} slots on"), &prices); } travel(&locker, now + DAY).await; - published("a time travel a day on"); + published("a time travel a day on", &prices); usdc_deposit(&locker, &graph).expect("a deposit a day on"); + + let btc_feed = graph.feed_of(custody_address(BTC_MINT)); + let mut aging = price_override(btc_feed, u64_at(graph.data(&btc_feed), feed::PRICE)); + aging.keep_alive = false; + play(&locker, vec![aging]).await; + let stopped_at = timestamp(&btc_feed); + travel(&locker, now + DAY + 20).await; + assert_eq!(timestamp(&btc_feed), stopped_at, "keep-alive off: BTC ages"); + let others: Vec<_> = prices.into_iter().filter(|(f, _)| *f != btc_feed).collect(); + published("20 seconds after BTC's keep-alive stops", &others); + let stale = usdc_deposit(&locker, &graph); + expect_error( + stale, + STALE_ORACLE_PRICE, + "a deposit on a 20-second-old BTC", + ); } /// A USDC deposit that works on the live pool, refused by each lever that guards deposits. From 20db0ecf00a1775c35f9572b04201b4b3cb47074 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 10 Nov 2026 07:48:13 +0200 Subject: [PATCH 09/11] docs(scenarios): say in each price template that it keeps every loaded price fresh --- .../scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml | 2 +- .../core/src/scenarios/protocols/jupiter-perps/v1/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml index 47b98ca95..93bfcf96f 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -17,7 +17,7 @@ keep_alive: templates: - id: jupiter-perps-doves-price name: Override Perps Oracle Price - description: Override the Doves price Jupiter Perps reads for one asset + description: Override the Doves price Jupiter Perps reads for one asset; keeps every loaded Doves feed published idl_account_name: AgPriceFeed properties: - path: price diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md index cc4d55b64..51b0665bb 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md @@ -8,8 +8,8 @@ Templates for the Jupiter Perps exchange and the JLP pool behind it, across two | Doves oracle | `DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e` | `jupiter-perps-doves` (`doves/v1`) | Perps reads every price from a Doves `AgPriceFeed`. It never reads the custody's Pyth account, so -`pyth-price-feed-v2` does not move Perps prices. Surfpool keeps a feed published on its own once a -scenario overrides it. +`pyth-price-feed-v2` does not move Perps prices. Once a scenario overrides one Doves price, Surfpool +keeps every loaded Doves feed published. # Template index From c3849276ceaf05ba5578baea6b6fc9d7ed7e6bd4 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 10 Nov 2026 08:09:14 +0200 Subject: [PATCH 10/11] test(scenarios): lift the pool weight limit in the Perps fee test so live weights cannot refuse its swaps --- crates/core/src/tests/jupiter_perps/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/core/src/tests/jupiter_perps/mod.rs b/crates/core/src/tests/jupiter_perps/mod.rs index a7b58e1fb..3e1279ea2 100644 --- a/crates/core/src/tests/jupiter_perps/mod.rs +++ b/crates/core/src/tests/jupiter_perps/mod.rs @@ -1360,7 +1360,15 @@ async fn pool_fees_set_deposit_withdrawal_and_swap_fees() { let (swap_bps, stable_bps) = (raised("swap_bps"), raised("stable_swap_bps")); let sol = graph.data(&custody_address(SOL_MINT)); let multiplier = BPS as u64 + u64_at(sol, custody::EXTERNAL_SWAP_FEE_MULTIPLIER_BPS); - let fees = |values: Value| vec![template_override(POOL_FEES, POOL, values)]; + // The test reads fees, so the pool limits must not refuse a swap that moves a token past its + // live target weight. + let any_weight = json!({ "limit.token_weightage_buffer_bps": "100000" }); + let fees = |values: Value| { + vec![ + template_override(POOL_FEES, POOL, values), + template_override(POOL_LIMITS, POOL, any_weight.clone()), + ] + }; let untaxed = json!({ "fees.add_remove_liquidity_bps": liquidity_bps, "fees.swap_bps": swap_bps, From fd042b09af2f15101fe12c11dc382eb1f7c8b2ca Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 10 Nov 2026 12:36:29 +0200 Subject: [PATCH 11/11] docs(scenarios): explain how to keep one Doves feed stale while the others stay fresh --- .../jupiter-perps/doves/v1/overrides.yaml | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml index 93bfcf96f..cd210fdc3 100644 --- a/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -25,7 +25,7 @@ templates: description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8" - path: timestamp label: Publish time - description: Unix seconds the price was published. Keep-alive writes the Clock time every slot + description: Unix seconds the price was published. Keep-alive writes the Clock time every slot, so a value set here holds only with keepAlive false address: type: pubkey llm_context: | @@ -46,12 +46,19 @@ templates: on a later override of the same feed in another scenario or slot: a reload drops the earlier local change. - THE PRICE STAYS PUBLISHED BY ITSELF. From the slot an override of this template plays, - Surfpool republishes every Doves feed the surfnet has loaded every slot at the Surfnet clock's - time, as the Doves publisher does on mainnet, including feeds a transaction loads later. A - JLP deposit, withdrawal, swap, quote or position finds every custody's feed fresh, and a - keeper fill finds the price published after its request. To keep one feed stale on purpose, - override it with keepAlive: false. + THE PRICE STAYS PUBLISHED BY ITSELF. From the slot an override of this template with + keepAlive: true plays, Surfpool republishes every Doves feed the surfnet has loaded every slot + at the Surfnet clock's time, as the Doves publisher does on mainnet, including feeds a + transaction loads later. A JLP deposit, withdrawal, swap, quote or position finds every + custody's feed fresh, and a keeper fill finds the price published after its request. + + STALE PRICE: keep-alive starts only when an override with keepAlive: true writes a feed, and + an override with no values writes nothing. To keep one feed stale while the others stay + fresh, add two overrides of this template, both with fetchBeforeUse: true and price set to + the feed's current price (getAccountInfo, encoding "jsonParsed"), so no price changes: + - on the stale feed, keepAlive: false. It then ages against the Surfnet clock; + - on any other feed, keepAlive: true. This starts the republishing of every loaded feed + except the stale one. LIQUIDATIONS: a keeper liquidates a position once collateral + PnL - close and borrow fees falls below size x 10000 / the custody's pricing.max_leverage. Move a long's price