diff --git a/crates/core/src/scenarios/README.md b/crates/core/src/scenarios/README.md index 4e7311a1c..29d676948 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; 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/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/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 new file mode 100644 index 000000000..cd210fdc3 --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml @@ -0,0 +1,74 @@ +protocol: jupiter-perps-doves +version: v1 +account_type: AgPriceFeed +idl_file_path: idl.json + +tags: + - oracle + - price-feed + - perpetuals + - defi + +keep_alive: + - template: jupiter-perps-doves-publication + address: { type: owned, owner: DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e, account: AgPriceFeed } + values: { timestamp: $unix_s } + +templates: + - id: jupiter-perps-doves-price + name: Override Perps Oracle Price + description: Override the Doves price Jupiter Perps reads for one asset; keeps every loaded Doves feed published + 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" + - path: timestamp + label: Publish time + 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: | + 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, and false + 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 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 + 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..51b0665bb --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md @@ -0,0 +1,181 @@ +# 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. Once a scenario overrides one Doves price, Surfpool +keeps every loaded Doves feed published. + +# 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. + +## Keep-alive (Surfpool handles it) + +On mainnet the Doves publisher republishes every feed's price within seconds. Nobody runs it +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. + +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 + +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 | 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 | +| 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 | 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` | + +# 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..b56fdaf3c --- /dev/null +++ b/crates/core/src/scenarios/protocols/jupiter-perps/v1/overrides.yaml @@ -0,0 +1,248 @@ +protocol: jupiter-perps +version: v1 +account_type: Multiple +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 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 + 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 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 + 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 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 + - 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 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 + (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 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 + 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 890545d17..6a1323b74 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -71,6 +71,17 @@ 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"); +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)] pub struct TemplateRegistry { @@ -134,6 +145,7 @@ impl TemplateRegistry { default.load_spl_token_overrides(); default.load_pump_overrides(); default.load_phoenix_overrides(); + default.load_jupiter_perps_overrides(); if let Err(e) = default.check_keep_alive_imports() { panic!("unable to load the override templates: {e}"); } @@ -338,6 +350,25 @@ 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", + ); + 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( &mut self, idl_content: &str, @@ -729,11 +760,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")); @@ -808,6 +840,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] @@ -1095,6 +1134,27 @@ 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::Owned { + owners: vec!["DoVEsk76QybCEHQGzkvYPWLQu9gzNoZZZt3TPiL597e".to_string()], + account: "AgPriceFeed".to_string(), + } + ); + 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) { @@ -1378,6 +1438,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] @@ -1387,8 +1458,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..3e1279ea2 --- /dev/null +++ b/crates/core/src/tests/jupiter_perps/mod.rs @@ -0,0 +1,1418 @@ +//! 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 = 465131236; +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| { + // 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(); + } + }); + 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); +} + +/// 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 { + 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 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, 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 = 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(); + 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 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, healthy).await; + let refused = liquidate(&locker, graph, KEEPER, at); + expect_error(refused, INVALID_POSITION_STATE, what); + play(&locker, 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 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, + market: &Market, + 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()); + travel(locker, 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, 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, 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, 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, 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, + 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) +} + +/// 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 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; + 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"); + travel(&locker, now + 5).await; + 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", + ); + + // 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); + (*feed, if *feed == sol_feed { sol_price } else { live }) + }) + .collect(); + 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 { + let data = account(&locker, feed).data; + 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 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"), &prices); + } + travel(&locker, now + DAY).await; + 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. +#[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, 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); + // 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, + "fees.stable_swap_bps": stable_bps, + "fees.tax_bps": 0, + "fees.stable_swap_tax_bps": 0, + }); + let locker = perps_vm(&graph).await; + play(&locker, 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_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; + 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:?}"); + let swapped = stable_swaps(); + let moved = swapped.iter().any(|bps| *bps != stable_bps); + assert!(moved, "taxed USDC and JupUSD swaps: {swapped:?}"); +} 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 d7efda601..b8f4c3237 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, }