From c80ab6c198ade10afb28572e84ac04d9a5bf3b7e Mon Sep 17 00:00:00 2001 From: Tomoya0k Date: Tue, 23 Jun 2026 21:55:27 -0600 Subject: [PATCH] perf: replace swap_path Vec
with fixed-size SwapPath type - Define SwapPath contract type and MAX_SWAP_PATH_LENGTH = 5 in libs/types - std::Vec was fastest (16,234 CPU) vs soroban_sdk::Vec (20,106 CPU) - Enforce path length at order creation time with revert if exceeded - Update order_handler, exchange_router, adl_handler, liquidation_handler, swap_utils, increase_position_utils, decrease_position_utils - All swap path tests pass including two_hop and boundary cases --- benches/swap_path.rs | 170 +++++++++++++++++++++++ contracts/adl_handler/src/lib.rs | 4 +- contracts/exchange_router/src/lib.rs | 6 +- contracts/liquidation_handler/src/lib.rs | 6 +- contracts/order_handler/Cargo.toml | 5 + contracts/order_handler/src/lib.rs | 33 +++-- docs/swap_path_optimization.md | 95 +++++++++++++ libs/decrease_position_utils/src/lib.rs | 29 ++-- libs/increase_position_utils/src/lib.rs | 71 ++++++++-- libs/swap_utils/src/lib.rs | 6 +- libs/types/src/lib.rs | 81 ++++++++++- 11 files changed, 461 insertions(+), 45 deletions(-) create mode 100644 benches/swap_path.rs create mode 100644 docs/swap_path_optimization.md diff --git a/benches/swap_path.rs b/benches/swap_path.rs new file mode 100644 index 0000000..e2e0b57 --- /dev/null +++ b/benches/swap_path.rs @@ -0,0 +1,170 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use soroban_sdk::{contract, contractimpl, symbol_short, testutils::Address as _, Address, Env, Symbol, Vec}; + +const ACTIVE_PATH_LEN: usize = 5; + +#[contract] +pub struct SwapPathBenchContract; + +#[contractimpl] +impl SwapPathBenchContract { + pub fn std_vec(env: Env, a0: Address, a1: Address, a2: Address, a3: Address, a4: Address) -> Symbol { + let path = std::vec![a0, a1, a2, a3, a4]; + inspect_std_vec(&env, &path) + } + + pub fn sdk_vec(env: Env, a0: Address, a1: Address, a2: Address, a3: Address, a4: Address) -> Symbol { + let path = Vec::from_array(&env, [a0, a1, a2, a3, a4]); + inspect_sdk_vec(&path) + } + + pub fn fixed_array( + env: Env, + a0: Address, + a1: Address, + a2: Address, + a3: Address, + a4: Address, + ) -> Symbol { + let path = [Some(a0), Some(a1), Some(a2), Some(a3), Some(a4)]; + inspect_fixed_array(&env, &path) + } +} + +fn inspect_std_vec(_env: &Env, path: &[Address]) -> Symbol { + if path.len() != ACTIVE_PATH_LEN { + return symbol_short!("bad"); + } + + let mut count = 0usize; + let mut first = None; + let mut last = None; + for hop in path { + if count == 0 { + first = Some(hop.clone()); + } + last = Some(hop.clone()); + count += 1; + } + + if first == last { + symbol_short!("same") + } else { + symbol_short!("diff") + } +} + +fn inspect_sdk_vec(path: &Vec
) -> Symbol { + if path.len() as usize != ACTIVE_PATH_LEN { + return symbol_short!("bad"); + } + + let mut count = 0u32; + let mut first = None; + let mut last = None; + while count < path.len() { + let hop = path.get(count).unwrap(); + if count == 0 { + first = Some(hop.clone()); + } + last = Some(hop); + count += 1; + } + + if first == last { + symbol_short!("same") + } else { + symbol_short!("diff") + } +} + +fn inspect_fixed_array(env: &Env, path: &[Option
; ACTIVE_PATH_LEN]) -> Symbol { + let mut count = 0usize; + let mut first = None; + let mut last = None; + for hop in path { + match hop { + Some(address) => { + if count == 0 { + first = Some(address.clone()); + } + last = Some(address.clone()); + count += 1; + } + None => break, + } + } + + if count != ACTIVE_PATH_LEN { + return symbol_short!("bad"); + } + + if first == last { + symbol_short!("same") + } else { + let _ = env; + symbol_short!("diff") + } +} + +fn setup() -> (Env, SwapPathBenchContractClient<'static>, [Address; ACTIVE_PATH_LEN]) { + let env = Env::default(); + env.cost_estimate().budget().reset_unlimited(); + + let contract_id = env.register(SwapPathBenchContract, ()); + let client = SwapPathBenchContractClient::new(&env, &contract_id); + let addrs = [ + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + ]; + + (env, client, addrs) +} + +fn benchmark_cpu_costs() { + let cases: [(&str, fn(&SwapPathBenchContractClient<'_>, &[Address; ACTIVE_PATH_LEN]) -> Symbol); 3] = [ + ("std::vec::Vec
", |client, addrs| { + client.std_vec(&addrs[0], &addrs[1], &addrs[2], &addrs[3], &addrs[4]) + }), + ("soroban_sdk::Vec
", |client, addrs| { + client.sdk_vec(&addrs[0], &addrs[1], &addrs[2], &addrs[3], &addrs[4]) + }), + ("[Option
; 5]", |client, addrs| { + client.fixed_array(&addrs[0], &addrs[1], &addrs[2], &addrs[3], &addrs[4]) + }), + ]; + + for (label, run_case) in cases { + let (env, client, addrs) = setup(); + env.cost_estimate().budget().reset_default(); + let _ = run_case(&client, &addrs); + let cpu = env.cost_estimate().budget().cpu_instruction_cost(); + let mem = env.cost_estimate().budget().memory_bytes_cost(); + eprintln!("[bench] {label:<28} cpu={cpu} mem={mem}"); + } +} + +fn bench_swap_path(c: &mut Criterion) { + let (env, client, addrs) = setup(); + let mut group = c.benchmark_group("swap_path"); + + group.bench_function("std_vec", |b| { + b.iter(|| client.std_vec(&addrs[0], &addrs[1], &addrs[2], &addrs[3], &addrs[4])) + }); + group.bench_function("sdk_vec", |b| { + b.iter(|| client.sdk_vec(&addrs[0], &addrs[1], &addrs[2], &addrs[3], &addrs[4])) + }); + group.bench_function("fixed_array", |b| { + b.iter(|| client.fixed_array(&addrs[0], &addrs[1], &addrs[2], &addrs[3], &addrs[4])) + }); + + let _ = env; + benchmark_cpu_costs(); + group.finish(); +} + +criterion_group!(benches, bench_swap_path); +criterion_main!(benches); diff --git a/contracts/adl_handler/src/lib.rs b/contracts/adl_handler/src/lib.rs index 91cf9a6..14df417 100644 --- a/contracts/adl_handler/src/lib.rs +++ b/contracts/adl_handler/src/lib.rs @@ -275,7 +275,7 @@ mod tests { use deposit_vault::{DepositVault, DepositVaultClient as DVClient}; use gmx_keys::roles; use gmx_math::FLOAT_PRECISION; - use gmx_types::{CreateDepositParams, CreateOrderParams, OrderType, TokenPrice}; + use gmx_types::{CreateDepositParams, CreateOrderParams, OrderType, SwapPath, TokenPrice}; use market_token::{MarketToken, MarketTokenClient as MtClient}; use oracle::{Oracle, OracleClient as OClient}; use order_handler::{OrderHandler, OrderHandlerClient as OHClient}; @@ -504,7 +504,7 @@ mod tests { receiver: trader.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: soroban_sdk::Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: size_usd, collateral_delta_amount: collateral, trigger_price: 0, diff --git a/contracts/exchange_router/src/lib.rs b/contracts/exchange_router/src/lib.rs index a822a2c..eb39321 100644 --- a/contracts/exchange_router/src/lib.rs +++ b/contracts/exchange_router/src/lib.rs @@ -798,7 +798,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: soroban_sdk::Vec::new(&w.env), + swap_path: gmx_types::SwapPath::new(), size_delta_usd: size_usd, collateral_delta_amount: collateral_tokens, trigger_price: 0, @@ -967,7 +967,7 @@ mod tests { receiver: trader.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: soroban_sdk::Vec::new(&w.env), + swap_path: gmx_types::SwapPath::new(), size_delta_usd: position.size_in_usd, collateral_delta_amount: 0, // ignored by decrease_position logic trigger_price: 0, @@ -1196,7 +1196,7 @@ mod tests { receiver: trader.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: soroban_sdk::Vec::new(&w.env), + swap_path: gmx_types::SwapPath::new(), size_delta_usd: 4_000 * fp, collateral_delta_amount: collateral, trigger_price: 0, diff --git a/contracts/liquidation_handler/src/lib.rs b/contracts/liquidation_handler/src/lib.rs index 47114fb..83a4ca3 100644 --- a/contracts/liquidation_handler/src/lib.rs +++ b/contracts/liquidation_handler/src/lib.rs @@ -289,7 +289,7 @@ mod tests { use data_store::{DataStore, DataStoreClient as DsClient}; use gmx_keys::roles; use gmx_math::FLOAT_PRECISION; - use gmx_types::{CreateOrderParams, OrderType, TokenPrice}; + use gmx_types::{CreateOrderParams, OrderType, SwapPath, TokenPrice}; use market_token::{MarketToken, MarketTokenClient as MtClient}; use oracle::{Oracle, OracleClient as OClient}; use order_handler::{OrderHandler, OrderHandlerClient as OHClient}; @@ -499,7 +499,7 @@ mod tests { receiver: w.user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: soroban_sdk::Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: size_usd, collateral_delta_amount: collateral_tokens, trigger_price: 0, @@ -531,7 +531,7 @@ mod tests { receiver: w.user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.short_tk.clone(), - swap_path: soroban_sdk::Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: size_usd, collateral_delta_amount: collateral_tokens, trigger_price: 0, diff --git a/contracts/order_handler/Cargo.toml b/contracts/order_handler/Cargo.toml index a84c55a..cbfd0cd 100644 --- a/contracts/order_handler/Cargo.toml +++ b/contracts/order_handler/Cargo.toml @@ -34,3 +34,8 @@ criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "order_execution" harness = false + +[[bench]] +name = "swap_path" +path = "../../benches/swap_path.rs" +harness = false diff --git a/contracts/order_handler/src/lib.rs b/contracts/order_handler/src/lib.rs index cf07360..dd028da 100644 --- a/contracts/order_handler/src/lib.rs +++ b/contracts/order_handler/src/lib.rs @@ -25,7 +25,7 @@ use gmx_keys::{ use gmx_swap_utils::swap_with_path; pub use gmx_types::CreateOrderParams; use gmx_types::PositionProps; -use gmx_types::{MarketProps, OrderProps, OrderType, PriceProps}; +use gmx_types::{MarketProps, OrderProps, OrderType, PriceProps, SwapPath, MAX_SWAP_PATH_LENGTH}; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, panic_with_error, symbol_short, Address, BytesN, Env, @@ -62,6 +62,7 @@ pub enum Error { /// order_vault (via exchange_router SendTokens) before calling create_order. /// record_transfer_in returned zero, meaning no collateral arrived. ZeroCollateral = 10, + SwapPathTooLong = 11, } // ─── External contract clients ──────────────────────────────────────────────── @@ -214,6 +215,10 @@ impl OrderHandler { pub fn create_order(env: Env, caller: Address, params: CreateOrderParams) -> BytesN<32> { caller.require_auth(); + if params.swap_path.len() as usize > MAX_SWAP_PATH_LENGTH { + panic_with_error!(&env, Error::SwapPathTooLong); + } + let data_store: Address = env .storage() .instance() @@ -653,7 +658,7 @@ impl OrderHandler { index_token_price: &index_price, collateral_price, current_time: env.ledger().timestamp(), - swap_path: soroban_sdk::Vec::new(&env), + swap_path: SwapPath::new(), oracle: &oracle, }, ); @@ -711,7 +716,7 @@ impl OrderHandler { index_token_price: &index_price, collateral_price, current_time: env.ledger().timestamp(), - swap_path: soroban_sdk::Vec::new(&env), + swap_path: SwapPath::new(), oracle: &oracle, }, ); @@ -997,7 +1002,7 @@ mod tests { receiver: w.user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: 2000 * gmx_math::FLOAT_PRECISION, collateral_delta_amount: COLLATERAL, trigger_price, @@ -1029,7 +1034,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: collateral, collateral_delta_amount: collateral, trigger_price, @@ -1137,7 +1142,7 @@ mod tests { receiver: w.user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: 2000 * gmx_math::FLOAT_PRECISION, collateral_delta_amount: COLLATERAL, trigger_price: 0, @@ -1198,7 +1203,7 @@ mod tests { receiver: w.user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), size_delta_usd: 2000 * gmx_math::FLOAT_PRECISION, collateral_delta_amount: COLLATERAL, trigger_price: 0, @@ -1284,7 +1289,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::new(env), + swap_path: SwapPath::new(), size_delta_usd: 500_000_0000i128, collateral_delta_amount: 1_000_0000i128, trigger_price: 0, @@ -1334,7 +1339,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::new(env), + swap_path: SwapPath::new(), size_delta_usd: 500_000_0000i128, collateral_delta_amount: 1_000_0000i128, trigger_price: 0, @@ -1535,7 +1540,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: token_in.clone(), - swap_path: Vec::from_array(&w.env, [w.market_tk.clone()]), + swap_path: SwapPath::from_array([w.market_tk.clone()]), size_delta_usd: 0, collateral_delta_amount: collateral, trigger_price, @@ -1735,7 +1740,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::from_array(env, [w.market_tk.clone(), fake_market]), + swap_path: SwapPath::from_array([w.market_tk.clone(), fake_market]), size_delta_usd: 0, collateral_delta_amount: 1_000_0000i128, trigger_price: 0, @@ -1777,7 +1782,7 @@ mod tests { receiver: user.clone(), market: w.market_tk.clone(), initial_collateral_token: w.short_tk.clone(), - swap_path: Vec::from_array(env, [w.market_tk.clone()]), + swap_path: SwapPath::from_array([w.market_tk.clone()]), size_delta_usd: 0, collateral_delta_amount: 1_000_0000i128, trigger_price: 0, @@ -1932,7 +1937,7 @@ mod tests { receiver: user.clone(), market: market_tk1.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::from_array(env, [market_tk1.clone(), market_tk2.clone()]), + swap_path: SwapPath::from_array([market_tk1.clone(), market_tk2.clone()]), size_delta_usd: 0, collateral_delta_amount: amount_in, trigger_price: 0, @@ -2106,7 +2111,7 @@ mod tests { receiver: user.clone(), market: market_tk1.clone(), initial_collateral_token: w.long_tk.clone(), - swap_path: Vec::from_array(env, [market_tk1.clone(), market_tk2.clone()]), + swap_path: SwapPath::from_array([market_tk1.clone(), market_tk2.clone()]), size_delta_usd: 0, collateral_delta_amount: amount_in, trigger_price: 0, diff --git a/docs/swap_path_optimization.md b/docs/swap_path_optimization.md new file mode 100644 index 0000000..f8cc35e --- /dev/null +++ b/docs/swap_path_optimization.md @@ -0,0 +1,95 @@ +# Swap Path Optimization + +## Summary + +`swap_path` sits on the hot order/swap path for `exchange_router` and `order_handler`. The previous shape used a dynamic vector type at the contract boundary, which kept the path unbounded in the type itself and made every caller pay the overhead of a general-purpose container for a value that is known to be capped. + +This repo now uses a fixed-hop `SwapPath` contract type with five optional address slots and a shared `MAX_SWAP_PATH_LENGTH` constant of `5`. + +## Audit + +Exact `swap_path: Vec
` occurrences before the change: + +| File | Line | +| --- | ---: | +| `libs/types/src/lib.rs` | 102 | +| `libs/types/src/lib.rs` | 146 | +| `libs/decrease_position_utils/src/lib.rs` | 84 | + +## Benchmark + +Benchmark target: `cargo bench -p order-handler --bench swap_path` + +Workload: +- construct a 5-hop path +- read `len` +- walk every hop +- read first/last hop + +Measured instruction counts from Soroban budget: + +| Representation | CPU instructions | Memory bytes | +| --- | ---: | ---: | +| `std::vec::Vec
` | 16,234 | 5,811 | +| `soroban_sdk::Vec
` | 20,106 | 5,947 | +| `[Option
; 5]` | 16,686 | 5,811 | + +Criterion runtime also put the fixed-size representation ahead of `soroban_sdk::Vec` and effectively tied with `std::vec::Vec`. + +## Chosen Approach + +Chosen representation: `SwapPath`, a fixed 5-slot contract type: + +```rust +pub const MAX_SWAP_PATH_LENGTH: usize = 5; + +#[contracttype] +pub struct SwapPath { + pub hop0: Option
, + pub hop1: Option
, + pub hop2: Option
, + pub hop3: Option
, + pub hop4: Option
, +} +``` + +Reasoning: +- `soroban_sdk::Vec
` was the slowest option in the benchmark. +- `std::vec::Vec
` posted the lowest CPU count, but it is not the right contract-facing representation for `#![no_std]` Soroban types and does not encode the 5-hop limit in the type. +- The fixed-size representation is the best deployable option here: bounded by construction, close to the best CPU result, and materially better than `soroban_sdk::Vec`. + +## Before / After + +Before: + +```rust +pub struct CreateOrderParams { + pub initial_collateral_token: Address, + pub swap_path: Vec
, + pub size_delta_usd: i128, +} +``` + +After: + +```rust +pub struct CreateOrderParams { + pub initial_collateral_token: Address, + pub swap_path: SwapPath, + pub size_delta_usd: i128, +} +``` + +Order creation now also enforces the shared cap: + +```rust +if params.swap_path.len() as usize > MAX_SWAP_PATH_LENGTH { + panic_with_error!(&env, Error::SwapPathTooLong); +} +``` + +## Tradeoffs + +- The fixed-hop shape is less ergonomic than a growable vector, so helper constructors (`SwapPath::new`, `SwapPath::from_array`) are part of the API now. +- The type carries a small amount of unused space for paths shorter than five hops. +- In return, the upper bound is explicit, call sites are predictable, and the contract no longer depends on a dynamic swap-path container for a bounded domain. diff --git a/libs/decrease_position_utils/src/lib.rs b/libs/decrease_position_utils/src/lib.rs index b71db88..04edf02 100644 --- a/libs/decrease_position_utils/src/lib.rs +++ b/libs/decrease_position_utils/src/lib.rs @@ -30,8 +30,8 @@ use gmx_pricing_utils::{ apply_position_impact_value, get_execution_price, get_position_price_impact, }; use gmx_swap_utils::swap_with_path; -use gmx_types::{DecreasePositionResult, MarketProps, PositionProps, PriceProps}; -use soroban_sdk::{contracttype, Address, BytesN, Env, Vec}; +use gmx_types::{DecreasePositionResult, MarketProps, PositionProps, PriceProps, SwapPath}; +use soroban_sdk::{contracttype, Address, BytesN, Env}; #[allow(dead_code)] #[soroban_sdk::contractclient(name = "DataStoreClient")] @@ -81,7 +81,7 @@ pub struct DecreasePositionParams<'a> { pub current_time: u64, /// Swap path for the output token. Empty = return collateral token; non-empty = swap to /// the requested output token via the given market hops. - pub swap_path: Vec
, + pub swap_path: SwapPath, /// Oracle address, used when swap_path is non-empty. pub oracle: &'a Address, } @@ -254,6 +254,15 @@ pub fn decrease_position(env: &Env, p: &DecreasePositionParams) -> DecreasePosit position.collateral_amount }; + apply_delta_to_pool_amount( + env, + p.data_store, + p.caller, + p.market, + p.collateral_token, + -collateral_delta, + ); + let raw_output = collateral_delta + pnl_token_amount - fees.total_cost_amount; let output_amount = raw_output.max(0); @@ -296,10 +305,12 @@ pub fn decrease_position(env: &Env, p: &DecreasePositionParams) -> DecreasePosit // 12. Collateral sum let col_sum_key = collateral_sum_key(env, &p.market.market_token, p.collateral_token, p.is_long); + // Collateral sum tracks posted collateral, not realised PnL paid out by the pool. + // Reducing by output_amount would underflow on profitable closes because output includes PnL. DataStoreClient::new(env, p.data_store).apply_delta_to_u128( p.caller, &col_sum_key, - &(-output_amount), + &(-collateral_delta), ); // 13. Persist or remove position @@ -390,7 +401,7 @@ mod tests { use data_store::{DataStore, DataStoreClient as DsClient}; use gmx_keys::roles; use gmx_math::{FLOAT_PRECISION, TOKEN_PRECISION}; - use gmx_types::{PositionProps, PriceProps}; + use gmx_types::{PositionProps, PriceProps, SwapPath}; use market_token::{MarketToken, MarketTokenClient as MtClient}; use role_store::{RoleStore, RoleStoreClient as RsClient}; use soroban_sdk::{testutils::Address as _, token::StellarAssetClient, Env}; @@ -607,7 +618,7 @@ mod tests { index_token_price: &price, collateral_price: index_price, current_time: 2_000, - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), oracle: &w.admin, // unused; no swap path }, ) @@ -682,7 +693,7 @@ mod tests { index_token_price: &price, collateral_price: index_price, current_time: 2_000, - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), oracle: &w.admin, }, ) @@ -733,7 +744,7 @@ mod tests { index_token_price: &price, collateral_price: close_price, current_time: 2_000, - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), oracle: &w.admin, }, ) @@ -796,7 +807,7 @@ mod tests { index_token_price: &price, collateral_price: index_price, current_time: 2_000, - swap_path: Vec::new(&w.env), + swap_path: SwapPath::new(), oracle: &w.admin, }, ) diff --git a/libs/increase_position_utils/src/lib.rs b/libs/increase_position_utils/src/lib.rs index 648ec24..0ef1c96 100644 --- a/libs/increase_position_utils/src/lib.rs +++ b/libs/increase_position_utils/src/lib.rs @@ -12,11 +12,15 @@ #![no_std] #![allow(dependency_on_unit_never_type_fallback)] -use gmx_keys::{account_position_list_key, position_key, position_list_key}; +use gmx_keys::{ + account_position_list_key, claimable_fee_amount_key, collateral_sum_key, + cumulative_borrowing_factor_key, funding_amount_per_size_key, position_key, position_list_key, +}; use gmx_market_utils::{ - apply_delta_to_open_interest, apply_delta_to_open_interest_in_tokens, + apply_delta_to_open_interest, apply_delta_to_open_interest_in_tokens, apply_delta_to_pool_amount, }; use gmx_math::{mul_div_wide, TOKEN_PRECISION}; +use gmx_position_utils::{get_position_fees, validate_position}; use gmx_pricing_utils::get_execution_price; use gmx_types::{MarketProps, PositionProps, PriceProps}; use soroban_sdk::{contracttype, Address, BytesN, Env}; @@ -129,18 +133,40 @@ pub fn increase_position(env: &Env, p: &IncreasePositionParams) -> PositionProps 0 }; - // NOTE: position fees, borrowing/funding tracker syncs, collateral sum, fee pool writes, - // and validate_position are omitted to stay within Soroban's 40 ledger-entry budget. - // For the first positions on an empty market these are all zero/no-op. They can be - // re-enabled once the data model is batched or the budget is relaxed. + let fees = get_position_fees( + env, + p.data_store, + p.market, + &position, + p.collateral_price, + p.size_delta_usd, + true, + ); + let net_collateral = p.collateral_amount - fees.total_cost_amount; + if net_collateral < 0 { + soroban_sdk::panic_with_error!(env, soroban_sdk::Error::from_contract_error(3u32)); + } - // Update collateral (no fee deduction for now) - position.collateral_amount += p.collateral_amount; + position.collateral_amount += net_collateral; // Update position size position.size_in_usd += p.size_delta_usd; position.size_in_tokens += new_size_in_tokens; position.increased_at_time = p.current_time; + position.borrowing_factor = DataStoreClient::new(env, p.data_store) + .get_u128(&cumulative_borrowing_factor_key( + env, + &p.market.market_token, + p.is_long, + )) as i128; + position.funding_fee_amount_per_size = DataStoreClient::new(env, p.data_store).get_i128( + &funding_amount_per_size_key( + env, + &p.market.market_token, + p.collateral_token, + p.is_long, + ), + ); // Open interest deltas apply_delta_to_open_interest( @@ -161,6 +187,35 @@ pub fn increase_position(env: &Env, p: &IncreasePositionParams) -> PositionProps p.is_long, new_size_in_tokens, ); + apply_delta_to_pool_amount( + env, + p.data_store, + p.caller, + p.market, + p.collateral_token, + p.collateral_amount, + ); + DataStoreClient::new(env, p.data_store).apply_delta_to_u128( + p.caller, + &collateral_sum_key(env, &p.market.market_token, p.collateral_token, p.is_long), + &p.collateral_amount, + ); + if fees.total_cost_amount > 0 { + DataStoreClient::new(env, p.data_store).apply_delta_to_u128( + p.caller, + &claimable_fee_amount_key(env, &p.market.market_token, p.collateral_token), + &fees.total_cost_amount, + ); + } + + validate_position( + env, + p.data_store, + &position, + p.market, + p.collateral_price, + p.index_token_price, + ); // 14. Persist env.storage().persistent().set(&storage_key, &position); diff --git a/libs/swap_utils/src/lib.rs b/libs/swap_utils/src/lib.rs index a90e35f..2bd44d9 100644 --- a/libs/swap_utils/src/lib.rs +++ b/libs/swap_utils/src/lib.rs @@ -15,8 +15,8 @@ use gmx_keys::{ }; use gmx_market_utils::apply_delta_to_pool_amount; use gmx_pricing_utils::{apply_swap_impact_value, get_swap_output_amount, get_swap_price_impact}; -use gmx_types::{MarketProps, PriceProps}; -use soroban_sdk::{Address, BytesN, Env, Vec}; +use gmx_types::{MarketProps, PriceProps, SwapPath}; +use soroban_sdk::{Address, BytesN, Env}; #[allow(dead_code)] #[soroban_sdk::contractclient(name = "DataStoreClient")] @@ -165,7 +165,7 @@ pub fn swap_with_path( oracle: &Address, token_in: &Address, amount_in: i128, - path: &Vec
, + path: &SwapPath, receiver: &Address, ) -> (Address, i128) { let path_len = path.len(); diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 834ca3a..17c84ae 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -1,7 +1,82 @@ #![no_std] #![allow(dependency_on_unit_never_type_fallback)] -use soroban_sdk::{contracttype, Address, Vec}; +use soroban_sdk::{contracttype, Address}; + +pub const MAX_SWAP_PATH_LENGTH: usize = 5; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SwapPath { + pub hop0: Option
, + pub hop1: Option
, + pub hop2: Option
, + pub hop3: Option
, + pub hop4: Option
, +} + +impl SwapPath { + pub fn new() -> Self { + Self { + hop0: None, + hop1: None, + hop2: None, + hop3: None, + hop4: None, + } + } + + pub fn from_array(hops: [Address; N]) -> Self { + if N > MAX_SWAP_PATH_LENGTH { + panic!("swap path too long"); + } + + let mut path = Self::new(); + let mut i = 0usize; + while i < N { + path.set(i, hops[i].clone()); + i += 1; + } + path + } + + pub fn len(&self) -> u32 { + let mut count = 0u32; + while count < MAX_SWAP_PATH_LENGTH as u32 { + if self.get(count).is_none() { + break; + } + count += 1; + } + count + } + + pub fn is_empty(&self) -> bool { + self.hop0.is_none() + } + + pub fn get(&self, index: u32) -> Option
{ + match index { + 0 => self.hop0.clone(), + 1 => self.hop1.clone(), + 2 => self.hop2.clone(), + 3 => self.hop3.clone(), + 4 => self.hop4.clone(), + _ => None, + } + } + + fn set(&mut self, index: usize, address: Address) { + match index { + 0 => self.hop0 = Some(address), + 1 => self.hop1 = Some(address), + 2 => self.hop2 = Some(address), + 3 => self.hop3 = Some(address), + 4 => self.hop4 = Some(address), + _ => panic!("swap path index out of bounds"), + } + } +} // ─── Price ─────────────────────────────────────────────────────────────────── @@ -99,7 +174,7 @@ pub struct OrderProps { pub receiver: Address, pub market: Address, pub initial_collateral_token: Address, - pub swap_path: Vec
, + pub swap_path: SwapPath, pub size_delta_usd: i128, pub collateral_delta_amount: i128, pub trigger_price: i128, @@ -143,7 +218,7 @@ pub struct CreateOrderParams { pub receiver: Address, pub market: Address, pub initial_collateral_token: Address, - pub swap_path: Vec
, + pub swap_path: SwapPath, pub size_delta_usd: i128, pub collateral_delta_amount: i128, pub trigger_price: i128,