Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions contracts/reader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use gmx_keys::{
market_short_token_key, open_interest_key, order_list_key, position_key, position_list_key,
saved_funding_factor_per_second_key, withdrawal_list_key, DEFAULT_KEEPER_HEARTBEAT_TIMEOUT,
};
use gmx_market_utils::{get_open_interest_for_side, get_pool_value};
use gmx_market_utils::{get_market_token_price, get_open_interest_for_side, get_pool_value};
use gmx_math::{mul_div_wide, TOKEN_PRECISION};
use gmx_position_utils::{get_position_fees, get_position_pnl_usd, is_liquidatable};
use gmx_pricing_utils::{get_execution_price, get_position_price_impact};
Expand Down Expand Up @@ -178,7 +178,38 @@ impl Reader {
maximize,
)
}

/// USD price per 1 market (GM) token, scaled to FLOAT_PRECISION.
/// Uses oracle max prices when maximize=true (conservative for minting),
/// min prices when maximize=false (conservative for burning).
/// Returns FLOAT_PRECISION ($1.00) when supply is zero (seed price).
pub fn get_market_token_price(
env: Env,
data_store: Address,
oracle: Address,
market: Address,
maximize: bool,
) -> i128 {
let market_props = Self::get_market(env.clone(), data_store.clone(), market);
let oracle_client = OracleClient::new(&env, &oracle);
let long_price = oracle_client
.get_primary_price(&market_props.long_token)
.mid_price();
let short_price = oracle_client
.get_primary_price(&market_props.short_token)
.mid_price();
let index_price = oracle_client
.get_primary_price(&market_props.index_token)
.mid_price();
get_market_token_price(
&env,
&data_store,
&market_props,
long_price,
short_price,
index_price,
maximize,
)
}
/// Get open interest for both sides of a market.
/// Returns (long_oi_usd, short_oi_usd).
pub fn get_open_interest(env: Env, data_store: Address, market_token: Address) -> (i128, i128) {
Expand Down
1 change: 1 addition & 0 deletions libs/market_utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ gmx-keys = { path = "../keys" }
soroban-sdk = { workspace = true, features = ["testutils"] }
role-store = { path = "../../contracts/role_store" }
data-store = { path = "../../contracts/data_store" }
market-token = { path = "../../contracts/market_token" }
proptest = "1"
88 changes: 88 additions & 0 deletions libs/market_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ mod tests {
use super::*;
use data_store::{DataStore, DataStoreClient as DsClient};
use gmx_keys::roles;
use market_token::MarketToken;
use role_store::{RoleStore, RoleStoreClient as RsClient};
use soroban_sdk::{testutils::Address as _, Env};

Expand Down Expand Up @@ -1091,6 +1092,35 @@ mod tests {
);
}

// ── Issue: get_market_token_price unit tests ──────────────────────────────────

/// Deposit 10,000 USD, supply = 10,000 GM → price = 1.00 USD
#[test]
fn market_token_price_one_dollar_at_parity() {
let env = Env::default();
env.mock_all_auths();
let (admin, ds, mt, it, lt, st) = make_market(&env);
let market = make_market_props(&mt, &it, &lt, &st);

let fp = FLOAT_PRECISION;
let ds_c = DsClient::new(&env, &ds);

// Pool: 10,000 USDC (short token @ $1)
let pool_tokens = 10_000_i128 * 10_000_000; // 10,000 tokens (7 decimals)
ds_c.apply_delta_to_u128(
&admin,
&gmx_keys::pool_amount_key(&env, &mt, &st),
&pool_tokens,
);

let price = fp; // $1
let info = get_pool_value(&env, &ds, &market, price, price, price, false);

// supply = 10,000 GM tokens
let supply = 10_000_i128 * 10_000_000;
let token_price = mul_div_wide(&env, info.pool_value, TOKEN_PRECISION, supply);

assert_eq!(token_price, fp, "price must be $1.00, got {token_price}");
// ── Issue #216: FundingRateSignFlipped event ──────────────────────────────

/// Seed all the config keys needed by compute_next_funding_factor.
Expand Down Expand Up @@ -1231,3 +1261,61 @@ mod tests {
let _delta = mul_div_wide(&env, delta_per_sec, dt, FLOAT_PRECISION);
}
}

/// After 200 USD in fees added to pool → price = 1.02 USD
#[test]
fn market_token_price_increases_after_fees() {
let env = Env::default();
env.mock_all_auths();
let (admin, ds, mt, it, lt, st) = make_market(&env);
let market = make_market_props(&mt, &it, &lt, &st);

let fp = FLOAT_PRECISION;
let ds_c = DsClient::new(&env, &ds);

// Pool: 10,200 USDC (original 10,000 + 200 fees)
let pool_tokens = 10_200_i128 * 10_000_000;
ds_c.apply_delta_to_u128(
&admin,
&gmx_keys::pool_amount_key(&env, &mt, &st),
&pool_tokens,
);

let price = fp; // $1
let info = get_pool_value(&env, &ds, &market, price, price, price, false);

// supply stays at 10,000 GM
let supply = 10_000_i128 * 10_000_000;
let token_price = mul_div_wide(&env, info.pool_value, TOKEN_PRECISION, supply);

assert_eq!(token_price, 1_020_000_000_000_i128 * (fp / 1_000_000_000_000),
"price must be $1.02, got {token_price}");
assert!(token_price > fp, "price must be above $1 after fees");
}

/// Zero supply → returns FLOAT_PRECISION (seed price $1) without panic
#[test]
fn market_token_price_zero_supply_returns_seed_price() {
let env = Env::default();
env.mock_all_auths();
let (_admin, ds, _mt, it, lt, st) = make_market(&env);

// Deploy market token at the expected address so total_supply() resolves
let market_token_addr = env.register(market_token::MarketToken, ());
let market = MarketProps {
market_token: market_token_addr,
index_token: it,
long_token: lt,
short_token: st,
};

let fp = FLOAT_PRECISION;
let price = fp;

// Supply is 0 (freshly deployed, no mints) → should return seed price
let token_price = get_market_token_price(&env, &ds, &market, price, price, price, false);

assert_eq!(token_price, fp, "zero supply must return seed price $1, got {token_price}");
}
}

Loading