Derived from docs/POOLS_REVIEEW.md. Addresses all 9 inconsistencies against GMX Synthetics in priority order.
| # | Issue | Severity | Workstream |
|---|---|---|---|
| 1 | bootstrap.sh omits market_type arg |
Blocker | Scripts |
| 2 | Bootstrap config writes are incomplete/stale | Blocker | Scripts |
| 3 | Test token architecture drifted from SAC to custom Soroban tokens | High | Token Architecture |
| 4 | Faucet owner model is test-only but must not leak | High | Token Architecture |
| 5 | Frontend uses symbolic IDs, not real contract addresses | High | Frontend / Config |
| 6 | GM token naming is generic (GMX Market Token / GM) |
Medium | Frontend / Config |
| 7 | Pool value is simplified — missing full PnL cap model | Medium | Pool Economics |
| 8 | Single-token pools have no config or execution guard | Medium | Pool Config |
| 9 | GLV pools are wired in frontend but not deployed | Low | GLV |
As of June 5, 2026, the testnet bootstrap blockers are resolved for the active custom-token path.
bootstrap.shnow passesmarket_type = sha256("DEFAULT")tomarket_factory.create_market.configure_market.shwrites the implemented GMX-style config keys that exist inlibs/keys.market_factoryregisters markets through the factory address soMARKET_KEEPERdoes not also needCONTROLLER.deploy.shgrantsCONTROLLERto the admin and market factory for testnet/operator configuration.- Single-token pools are explicitly rejected until the single-token execution paths are audited.
- Active testnet tokens are custom Soroban faucet tokens:
TUSDC,TWBTC,TETH, andTXLM. - Frontend config export now emits real Soroban contract addresses for all bootstrapped markets.
Bootstrapped testnet markets:
TWBTC/TUSDC market_token = CDDVSLBGGDV2UOFN5W72R4LW7ABYL7H7ZWVSFHGMXXB3D52ZYANC5G3L
TETH/TUSDC market_token = CCBUUSYZJTGVA6PYUNQDFPZFHTBZ2QSHOUO7YAGRQVA46T3ZLSIYULS4
TXLM/TUSDC market_token = CDIBR7BDCDWGAG3CC6PBKRSLMISPYKNDGE57DCZO5TMTLZK34TMGKFQQ
Generated frontend files:
.deployed/frontend-testnet.env
.deployed/frontend-testnet.ts
Remaining MVP work after this pass: submit/verify oracle prices, seed initial liquidity, and run an end-to-end deposit/withdraw/order smoke test against these real market token IDs.
Root cause. market_factory::create_market signature is:
create_market(caller, index_token, long_token, short_token, market_type: BytesN<32>)
The script calls it without market_type, making every scripted market creation fail at the ABI boundary.
Fix steps:
- In
scripts/bootstrap.sh, compute the market type hash before thecreate_marketcall:Or, if a helper already exists in the contract, call it. Otherwise hardcode the known SHA-256 ofMARKET_TYPE=$(stellar contract invoke \ --id "$DATA_STORE" \ -- sha256 "DEFAULT" 2>/dev/null \ || python3 -c "import hashlib, sys; print(hashlib.sha256(b'DEFAULT').hexdigest())")
"DEFAULT"as a 32-byte hex string. - Pass
--market-type "$MARKET_TYPE"to everycreate_marketinvocation in the script. - Add the same
market_typeargument to anymake bootstrapMake target that shells out tobootstrap.sh. - Write a smoke test:
make bootstrapon a fresh testnet deployment must succeed end-to-end without error.
Files to touch:
scripts/bootstrap.shmx/common.mkorMakefile(any Make targets that call bootstrap)
Root cause. GMX markets require a full set of per-market config keys written into data_store. The current script attempts pool_amount_key on MARKET_FACTORY (wrong contract) and leaves most risk/fee keys unset.
Required config keys per market (mirroring gmx_keys in the Rust codebase):
| Key category | Keys |
|---|---|
| Pool size | max_pool_amount, max_pool_amount_for_deposit |
| Open interest | max_open_interest (long + short) |
| Reserve | reserve_factor (long + short), open_interest_reserve_factor |
| Borrowing | borrowing_factor, borrowing_exponent_factor (long + short) |
| Funding | funding_factor, funding_exponent_factor, funding_increase_factor_per_second, funding_decrease_factor_per_second, max_funding_factor_per_second, min_funding_factor_per_second |
| Swap impact | swap_impact_factor (positive + negative), swap_impact_exponent_factor |
| Position impact | position_impact_factor (positive + negative), position_impact_exponent_factor, max_position_impact_factor, max_position_impact_factor_for_liquidations |
| Fees | swap_fee_factor, position_fee_factor |
| PnL | max_pnl_factor (deposit/withdrawal/trader — long + short), min_collateral_factor, min_collateral_factor_for_open_interest |
| Price impact | price_impact_pool_amount |
Fix steps:
- Create
scripts/configure_market.sh <MARKET_TOKEN> <NETWORK> <SOURCE>:- Reads a TOML/JSON config file (e.g.
config/markets/TWBTC-TUSDC.toml) for default values. - Iterates over all required keys and calls
data_store set_*for each.
- Reads a TOML/JSON config file (e.g.
- Add a
config/markets/directory with per-market TOML files:config/markets/default.toml # shared base values config/markets/TWBTC-TUSDC.toml # overrides for BTC/USD config/markets/TETH-TUSDC.toml config/markets/TXLM-TUSDC.toml - Update
scripts/bootstrap.shto callconfigure_market.shafter eachcreate_market. - Add a
make configure-marketsMake target for re-running config writes without full redeploy. - Remove the broken
pool_amount_keycall onMARKET_FACTORY.
Files to touch:
scripts/bootstrap.shscripts/configure_market.sh(new)config/markets/(new directory + TOML files)Makefile/mx/common.mk
Decision required. Choose one canonical path and enforce it everywhere:
| Path | When to use | Tradeoffs |
|---|---|---|
test_token + test_faucet |
Demos, UX self-service, frontend testing | Simpler ops; not real Stellar asset plumbing |
| Stellar classic SAC | Production-like behavior, trustlines, real asset semantics | More ops overhead; matches real collateral flow |
Fix steps (if choosing test_token path — the current deployed reality):
- Update
TEST_ASSETS.mdto remove any claim that these are SAC-wrapped classic assets; describe them as custom mintable Soroban tokens. - Search and remove all references to SAC-specific tooling (
wrap,ASSET=, trustline steps) from the primary bootstrap flow. - Keep the SAC section in
TEST_ASSETS.mdbut mark it## SAC Path (Alternative)and note it is not the active testnet path. - Update all Make targets in
mx/tokens.mkto reflect which path is active and avoid mixing both in a singlebootstraprun.
Fix steps (if choosing SAC path — revert to canonical):
- Remove
contracts/test_tokenandcontracts/test_faucetfrom active bootstrap path. - Restore SAC deploy steps as the default
make market-tokenstarget. - Keep
test_token/test_faucetas an optional "demo mode" Make target.
Files to touch:
TEST_ASSETS.md(→docs/TEST_ASSETS.md)mx/tokens.mkMakefiledocs/FRONTEND_TESTNET_FAUCET.md(update deployed IDs if path changes)
Root cause. Each test_token is initialized with the faucet contract as owner. This allows the faucet to mint to any user. This model is only correct for testnet.
Fix steps:
- Add a compile-time or runtime guard in
contracts/test_token/src/lib.rs:Or, enforce at the Make/CI level: the// Reject initialization on mainnet network passphrase const MAINNET_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015";
test-tokens-with-faucettarget must only run withNETWORK=testnet. - Document the token list to deploy on testnet:
TUSDC - stable short/collateral TWBTC - BTC/USD long + index TETH - ETH/USD long + index TXLM - XLM/USD long + index - Add
TETHandTXLMto the faucet deployment script and Make targets (currently onlyTWBTC+TUSDCare deployed). - Add a
faucet-disable-tokenMake target so any token can be disabled in the faucet without redeployment. - Write CI check: if
NETWORK=mainnetis passed, Make must exit non-zero before deploying test contracts.
Files to touch:
contracts/test_token/src/lib.rscontracts/test_faucet/src/lib.rsmx/tokens.mkMakefile
Root cause. Frontend uses strings like BTC-BTC-USDC, BTC, USDC. Protocol contracts require real Soroban Address values (32-byte C-prefixed strings).
Fix steps:
- After each
make bootstraprun,bootstrap.shmust emit all generated IDs into a frontend-consumable env file:# scripts/bootstrap.sh — at the end cat > .deployed/frontend-testnet.env <<EOF MARKET_TWBTC_TUSDC=$MARKET_TOKEN_TWBTC TOKEN_TWBTC=$TWBTC TOKEN_TUSDC=$TUSDC TOKEN_TETH=$TETH TOKEN_TXLM=$TXLM MARKET_TETH_TUSDC=$MARKET_TOKEN_TETH MARKET_TXLM_TUSDC=$MARKET_TOKEN_TXLM EOF
- Create
scripts/export_frontend_config.shthat reads.deployed/testnet.env+.deployed/tokens-testnet.envand produces a typed TypeScript constants file:// generated — do not edit export const MARKETS = { "TWBTC/TUSDC": { marketToken: "C...", indexToken: "C...", longToken: "C...", shortToken: "C..." }, ... } as const;
- Add a
make export-frontend-configMake target that runs this script. - Communicate to the frontend team: symbolic strings must be replaced with values from
MARKETSbefore any contract call. - Add validation in the frontend SDK layer that throws if a non-address string is passed to a contract method.
Files to touch:
scripts/bootstrap.shscripts/export_frontend_config.sh(new)Makefile
Root cause. Every market_token is initialized with name = "GMX Market Token" and symbol = "GM". The frontend/indexer cannot distinguish markets by token metadata alone.
Fix steps:
- Change
market_tokeninitialization to acceptnameandsymbolfrommarket_factory:Or keep// market_factory: pass descriptive name and symbol let name = format!("{}/{} Market", index_symbol, short_symbol); let symbol = format!("GM-{}", index_symbol); // e.g. "TWBTC/TUSDC Market", symbol "GM-TWBTC"
GMas the symbol but pass a name like"SO4 TWBTC/TUSDC". - Alternatively (lower-risk), keep the current generic metadata but ensure the frontend resolves market identity exclusively from
data_storefields (index_token,long_token,short_token), never from the tokenname/symbol. - In
readeror a newmarket_readerhelper, expose aget_market_label(market_token) -> Stringview that derives the display label from stored token addresses. - Update
docs/POOLS_REVIEEW.mdto mark this resolved once one of the above approaches is implemented.
Files to touch:
contracts/market_factory/src/lib.rscontracts/market_token/src/lib.rs- Frontend config / SDK layer
Root cause. market_utils::get_pool_value sets total_borrowing_fees: 0 and does not implement GMX's max_pnl_factor model, which applies different PnL cap factors for:
- deposit operations
- withdrawal operations
- trader operations
This affects pool token price calculation and therefore deposit/withdrawal output amounts.
Fix steps (MVP documentation):
- Add a
// SIMPLIFIED: total_borrowing_fees always 0; full borrowing fee accrual not yet implementedcomment inmarket_utils.rsat the relevant line. - Add a
// SIMPLIFIED: PnL cap factor not applied; pool value may diverge from GMX model under large open interestcomment. - Create
docs/POOL_ECONOMICS_GAPS.mdlisting exactly what is simplified and the intended full implementation (see below).
Fix steps (full implementation — target after MVP):
- Implement borrowing fee accrual: track
cumulative_borrowing_factorper side indata_storeand update it on every position event. - Apply
max_pnl_factoringet_pool_value:let pnl_factor_type = match op { PoolValueOp::Deposit => PnlFactorType::MaxPnlFactorForDeposits, PoolValueOp::Withdrawal => PnlFactorType::MaxPnlFactorForWithdrawals, PoolValueOp::Trader => PnlFactorType::MaxPnlFactorForTraders, }; let capped_pnl = apply_pnl_cap(raw_pnl, pool_usd, pnl_factor_type, market);
- Wire
get_pool_valuecallers (deposit_handler,withdrawal_handler,reader) to pass the correctPoolValueOpvariant. - Add property tests comparing pool value output before and after a sequence of deposits and withdrawals against expected invariants.
Files to touch:
contracts/market_utils/src/lib.rscontracts/deposit_handler/src/lib.rscontracts/withdrawal_handler/src/lib.rsdocs/POOL_ECONOMICS_GAPS.md(new)
Root cause. GMX supports longToken == shortToken (single-token pools) with dedicated swap and price-impact logic. SO4 may technically allow equal token addresses in create_market but the config and execution paths are not validated for this case.
Fix steps:
- In
market_factory::create_market, add an explicit check:if long_token == short_token { // Either reject until single-token logic is proven: return Err(Error::SingleTokenPoolNotSupported); // Or set a flag in data_store marking this market as single-token }
- If rejecting for now, document in the error enum with a
// TODO: single-token pool supportnote. - If supporting it, audit
deposit_handler,withdrawal_handler,order_handler, andmarket_utilsfor all code paths that assumelong_token != short_tokenand addis_single_token_poolbranches accordingly. - Do not advertise single-token GM pools in the frontend until this is complete.
Files to touch:
contracts/market_factory/src/lib.rscontracts/market_utils/src/lib.rs(if implementing support)- Frontend market config
Root cause. GLV aggregates liquidity across multiple GM markets and rebalances based on allocation/utilization rules. Frontend has GLV concepts but no GLV contracts are deployed.
Fix steps (short-term — disable GLV cleanly):
- In the frontend env/config, add a feature flag:
export const FEATURES = { glv: false, // GLV contracts not yet deployed } as const;
- Guard all GLV UI paths behind
FEATURES.glv. - In
docs/, add aGLV_IMPLEMENTATION_PLAN.mdnoting GLV is out of scope for the current milestone.
Fix steps (full GLV implementation — future milestone):
- Design and implement
glv_tokencontract (aggregated LP token). - Design and implement
glv_routercontract with:deposit_to_glv(glv, gm_markets[], amounts[])— allocates proportionallywithdraw_from_glv(glv, shares)— redeems from underlying marketsrebalance(glv)— shifts liquidity per utilization targets
- Add
glv_factoryto deploy new GLV vaults for sets of GM markets. - Wire GMX-style allocation/utilization rules: max allocation per market, utilization thresholds.
- Deploy on testnet and connect to real GM market token addresses.
Files to touch:
contracts/glv_token/(new)contracts/glv_router/(new)contracts/glv_factory/(new)- Frontend feature flag config
Follow this order to unblock frontend and testnet usage quickly:
Phase 1 — Blockers (do these first, they break everything else)
[ ] Issue 1: Fix bootstrap.sh to pass market_type
[ ] Issue 2: Add configure_market.sh with full config key writes
Phase 2 — Token Architecture (unblocks end-to-end testing)
[ ] Issue 3: Decide and document canonical test token path (SAC vs custom)
[ ] Issue 4: Deploy TUSDC + TWBTC + TETH + TXLM via faucet; add mainnet guard
Phase 3 — Frontend / Config (unblocks frontend integration)
[ ] Issue 5: Export real contract addresses from bootstrap into frontend env
[ ] Issue 6: Fix GM token naming or add market label resolver in reader
Phase 4 — Pool Economics (unblocks accurate LP pricing)
[ ] Issue 7 (MVP): Add simplified-economics comments and create POOL_ECONOMICS_GAPS.md
[ ] Issue 7 (full): Implement borrowing fee accrual + PnL cap model
Phase 5 — Advanced Pool Types (gates future feature work)
[ ] Issue 8: Guard or implement single-token pool logic
[ ] Issue 9: Disable GLV in frontend with feature flag; plan GLV contracts
For each of the three target markets, the canonical shape is:
TWBTC/TUSDC
index_token = TWBTC contract ID
long_token = TWBTC contract ID
short_token = TUSDC contract ID
market_type = sha256("DEFAULT")
TETH/TUSDC
index_token = TETH contract ID
long_token = TETH contract ID
short_token = TUSDC contract ID
market_type = sha256("DEFAULT")
TXLM/TUSDC
index_token = TXLM contract ID
long_token = TXLM contract ID
short_token = TUSDC contract ID
market_type = sha256("DEFAULT")
All three are fully-backed markets (index == long). No single-token or synthetic markets until Phase 5 is complete.
- POOLS_REVIEEW.md — original review notes this plan is derived from
- TEST_ASSETS.md — test token deployment and configuration
- FRONTEND_TESTNET_FAUCET.md — frontend faucet integration guide
- DEPLOYMENT_CMD.md — deployment command reference