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 e7beacf..24f16c5 100644 --- a/contracts/exchange_router/src/lib.rs +++ b/contracts/exchange_router/src/lib.rs @@ -953,7 +953,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, @@ -1122,7 +1122,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, @@ -1436,7 +1436,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 4df81c8..40a0c68 100644 --- a/contracts/liquidation_handler/src/lib.rs +++ b/contracts/liquidation_handler/src/lib.rs @@ -291,7 +291,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}; @@ -501,7 +501,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, @@ -533,7 +533,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 b37025b..fafc783 100644 --- a/contracts/order_handler/Cargo.toml +++ b/contracts/order_handler/Cargo.toml @@ -38,3 +38,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 619a806..4aff1ca 100644 --- a/contracts/order_handler/src/lib.rs +++ b/contracts/order_handler/src/lib.rs @@ -31,7 +31,7 @@ use gmx_math::{mul_div_wide, FLOAT_PRECISION}; 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, contractevent, contractimpl, contracttype, panic_with_error, symbol_short, Address, BytesN, Env, @@ -69,18 +69,21 @@ pub enum Error { /// order_vault (via exchange_router SendTokens) before calling create_order. /// record_transfer_in returned zero, meaning no collateral arrived. ZeroCollateral = 10, + perf/swap-path-clean UnauthorizedPositionManager = 11, + + SwapPathTooLong = 12, /// `flag_stale_keeper` was called but the role's last activity is still /// within the configured heartbeat timeout (issue #249). - KeeperNotStale = 12, + KeeperNotStale = 13, /// Position size/collateral ratio exceeds configured maximum leverage. - MaxLeverageExceeded = 13, + MaxLeverageExceeded = 14, /// `create_orders` received more than 5 orders in a single batch. - BatchSizeLimitExceeded = 14, + BatchSizeLimitExceeded = 15, /// The target market is paused due to circuit breaker (issue #203). - MarketPaused = 15, + MarketPaused = 16, /// swap_path contains a repeated market address — would corrupt pool accounting (issue #232). - CyclicSwapPath = 16, + CyclicSwapPath = 17, } @@ -134,6 +137,7 @@ pub struct PositionLiquidatedEvent { pub market: Address, pub execution_price: i128, pub remaining_collateral: i128, + main } // ─── External contract clients ──────────────────────────────────────────────── @@ -520,6 +524,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() @@ -1162,7 +1170,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, }, ); @@ -1227,7 +1235,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, }, ); @@ -1587,7 +1595,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, @@ -1619,7 +1627,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, @@ -1727,7 +1735,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, @@ -1788,7 +1796,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, @@ -1874,7 +1882,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, @@ -1924,7 +1932,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, @@ -2125,7 +2133,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, @@ -2325,7 +2333,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, @@ -2367,7 +2375,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, @@ -2522,7 +2530,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, @@ -2696,7 +2704,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 a5cccf4..50720f7 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}; @@ -603,7 +614,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 }, ) @@ -678,7 +689,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, }, ) @@ -729,7 +740,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, }, ) @@ -792,7 +803,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 857e018..80b6abe 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 8a17810..7a15eb8 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