Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .changelog/unreleased/642-dx1-versioning-feedback-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
type: added
area: docs
pr: 642
breaking: false
---

Documentation pages now carry a cookieless "was this helpful" control, and
the docs site can serve archived versions under a `/<version>` prefix with a
version picker, alongside new `/concepts/unified-liquidity` and a rewritten
`/concepts/funding-and-fees` page.
3 changes: 3 additions & 0 deletions apps/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.nitro-static/
.nitro/
.output/
# DX-050: version snapshots are reproducible via scripts/snapshot-version.ts
# and are not authored content — see that script's header comment.
content-versions/
89 changes: 84 additions & 5 deletions apps/docs/content/concepts/funding-and-fees.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,91 @@
---
title: Funding and fees
description: How funding, transaction charges, borrowing costs, and price impact affect a perpetual position.
updated: 2026-08-24
description: Every cost a leveraged position can accrue on SO4 — position fee, price impact, funding, borrow fee, and the network fee — with a worked multi-interval example.
updated: 2026-08-31
status: stable
---

Trading cost is more than a displayed fee because time, market balance, execution, and the Stellar transaction can each affect the result.
Opening or holding a position on SO4 can cost five distinct things: a position fee when size changes, price impact on the entry or exit price, recurring <Term id="funding">funding</Term> while the position stays open, a borrow fee on the collateral it locks up, and a network fee paid to the keeper that executes the order. All five are computed client-side today; none of them read a live value from the deployed contracts yet, which the [gaps](#where-the-client-diverges-from-the-contracts) section below covers in detail.

<Term id="funding">Funding</Term> transfers value between long and short positions. <Term id="price-impact">Price impact</Term> changes the execution estimate as a trade changes market exposure. <Term id="open-interest">Open interest</Term> describes positions that remain active.
## The five costs

Always review the transaction simulation and wallet prompt before signing. See [risk](/concepts/risk) for failure modes.
### Position fee

Charged whenever a position's size changes, on both the increase (open) and the decrease (close) side, at `positionFeeBps` from `DEFAULT_FEE_CONFIG` — currently 10 basis points (0.10%) of the size delta. `useTradeFees.ts` accepts an `isIncrease` parameter but does not use it to vary the rate, so opening and closing the same size cost the same fee. A `Swap` trade uses `swapFeeBps` instead, also 10 basis points today.

Source: `positionFeeBps` and `swapFeeBps`, `apps/web/src/features/trade/lib/data-store.ts`; applied in `apps/web/src/features/trade/hooks/useTradeFees.ts`.

### Price impact

An estimate shown as `priceImpactUsd` and, in the trade panel, as a percentage move applied to the entry price (`getEstimatedEntryPrice` in `apps/web/src/features/trade/lib/pricing.ts`). In the current client this is a flat `PRICE_IMPACT_BPS = 5` (0.05%) applied to every trade regardless of size or which side of the pool it falls on — not a function of pool imbalance, despite what the name suggests a GMX-style system would compute.

Source: `PRICE_IMPACT_BPS`, `apps/web/src/features/trade/hooks/useTradeFees.ts`.

### Funding

A recurring transfer between the long and short sides of a market, intended to keep the market's price near the reference price named in [oracles](/concepts/oracles). `useFundingRate.ts` reads a signed `funding_factor_per_second` from `SyntheticsReaderClient.get_funding_info` — the comment on the generated type says it "mirrors `gmx_types::FundingInfo`" — and converts it to a fractional per-hour rate: `ratePerHour = fundingFactorPerSecond × 3600 / 10^30`. Under the GMX v2 convention this contract mirrors, a positive rate means the side with larger <Term id="open-interest">open interest</Term> pays the smaller side; SO4's client does not separately label which side is paying, so read the sign from the raw rate.

Funding settles in fixed 8-hour epochs (`FUNDING_INTERVAL_MS` in `useFundingRate.ts`); the trade panel's `FundingRate` component (`apps/web/src/features/trade/components/FundingRate.tsx`) shows the current per-hour rate next to a countdown to the next epoch boundary, not the amount accrued so far.

Source: `useFundingRate.ts`, `FundingRate.tsx`.

### Borrow fee

A fee for holding an open position, charged against the collateral it locks up rather than against trade size. `DEFAULT_FEE_CONFIG.borrowingRatePerHour` sets it to `0.0001` (0.01%/hour) in `data-store.ts`, and `useMarketsInfo.ts` reports the same fixed `0.0001` per market. A separate, unused helper, `estimateBorrowFeePerHour` in `trade-math.ts`, computes a different figure from a `borrowingFactorBps` parameter divided by both `10_000` and `24` — the extra division by 24 does not match a function documented as a per-hour rate, and this helper is not called from `useTradeFees` or any component today.

Source: `borrowingRatePerHour`, `apps/web/src/features/trade/lib/data-store.ts`; `estimateBorrowFeePerHour`, `apps/web/src/features/trade/lib/trade-math.ts`.

### Network (execution) fee

Paid to the keeper that executes the order in a second transaction, shown in the trade panel as "Execution fee" with the tooltip "Paid to network keepers who execute your order." `minExecutionFeeXlm` is `0.3` XLM in `DEFAULT_FEE_CONFIG`; `useTradeFees.ts` converts it to a USD estimate using the current XLM mid price, falling back to `0.17` USD/XLM if no live price is available.

Source: `minExecutionFeeXlm`, `data-store.ts`; conversion in `useTradeFees.ts`.

## Summary

Values below are current as of `apps/web/src/features/trade/lib/data-store.ts` and `apps/web/src/features/trade/hooks/useTradeFees.ts` on this branch; funding is the one row read live per market rather than configured client-side.

| Cost | Current value | Charged when |
| --- | --- | --- |
| Position fee | 0.10% of size (10 bps) | Every increase or decrease of size |
| Swap fee | 0.10% of size (10 bps) | A `Swap`-type trade |
| Price impact | 0.05% of size (5 bps), flat | Every trade, same rate regardless of size |
| Funding | Signed rate from `SyntheticsReader`, per-hour, settled every 8 hours | Continuously, while a position is open |
| Borrow fee | 0.01%/hour (1 bp) of collateral | Continuously, while a position is open — not currently shown in the trade panel |
| Network (execution) fee | 0.3 XLM minimum per keeper execution | Once per increase and once per decrease |

## Worked example: costs over a held position

A trader opens a $10,000 long position and holds it across three consecutive 8-hour funding epochs (24 hours) before closing it. Assume the XLM price used for the execution-fee estimate is $0.17, and assume `SyntheticsReader.get_funding_info` for this market yields a `ratePerHour` of 0.00003 (0.003%/hour) for the whole window — an illustrative rate, not a value read from a deployed contract.

**Open:**

- Position fee: `10,000 × 0.0010 = $10.00`
- Price impact: `10,000 × 0.0005 = $5.00`
- Execution fee: `0.3 × 0.17 = $0.051`
- Open subtotal: `$15.051`

**Held for 24 hours (3 × 8-hour epochs), funding only — the client does not accrue a borrow fee into any total today:**

- Funding per hour: `10,000 × 0.00003 = $0.30`
- Funding over 24 hours: `0.30 × 24 = $7.20`

**Close** (the client charges the same position fee and price impact rate on a decrease as on an increase, and a second execution fee for the keeper that executes the close):

- Position fee: `10,000 × 0.0010 = $10.00`
- Price impact: `10,000 × 0.0005 = $5.00`
- Execution fee: `0.3 × 0.17 = $0.051`
- Close subtotal: `$15.051`

**Total for the round trip:** `$15.051 + $7.20 + $15.051 = $37.302` — of which funding, the only cost that grows with how long the position stays open, is $7.20 for this one day. Held for a week at the same rate instead of a day, funding alone would be `10,000 × 0.00003 × 24 × 7 = $50.40`, more than three times the combined open and close costs.

## Where the client diverges from the contracts {#where-the-client-diverges-from-the-contracts}

- `fetchFeeConfig` in `data-store.ts` returns a hard-coded `DEFAULT_FEE_CONFIG` for every market; despite its name, it does not read DataStore. Position fee, swap fee, and the execution-fee minimum are the same for every market until that changes.
- Price impact is a flat rate, not a function of trade size or which side of the pool a trade adds to or removes from — see [price impact](#price-impact) above.
- The borrow fee is configured in two places that disagree (`data-store.ts` and `trade-math.ts`) and is not applied to `totalFeesUsd` in `useTradeFees.ts` or shown as a row in the trade panel. A position accrues it in the protocol regardless of what the interface currently displays.
- Funding is the one cost read live from a contract (`SyntheticsReader.get_funding_info`); everything else in this page is a client-side estimate that can differ from what the transaction actually settles for.

## Related

[/concepts/liquidation](/concepts/liquidation) — funding and borrow fees reduce the collateral backing a position, which moves its liquidation price. [/concepts/oracles](/concepts/oracles) — the price funding and price impact are measured against. [/concepts/risk](/concepts/risk) — the broader list of what can go wrong, including these costs being estimates.
70 changes: 70 additions & 0 deletions apps/docs/content/concepts/unified-liquidity.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Unified liquidity
description: Why one GM pool backs both sides of a market's trades, what a depositor is exposed to, and how the pool's balance limits leverage and price impact.
updated: 2026-08-31
status: stable
---

Every SO4 market has exactly one pool, and that pool is the counterparty to every trade on the market — long or short, opening or closing. There is no separate book for longs and no separate book for shorts; a single balance of the long token and TUSDC backs both sides at once. This is what "unified liquidity" means in practice: one deposit funds every position, in either direction, that the market can hold.

## How this differs from isolated liquidity

An isolated-liquidity design gives each side of a market — or each market entirely — its own segregated pool, so a loss on one book cannot touch capital sitting in another. SO4's GM pool design does the opposite on purpose: the same TWBTC/TUSDC pool, for example, backs every long and every short on BTC/USD, and a liquidity provider's deposit is exposed to both sides simultaneously rather than to a single direction.

The trade-off is capital efficiency against concentration. A unified pool needs less total capital to support a given amount of open interest, because a long trader's gain and a short trader's loss can net out of the same balance instead of requiring two separately funded books. It also means a depositor cannot choose to fund only the long side or only the short side — the deposit backs whatever the market's traders are doing, in aggregate, at all times.

## What a depositor is exposed to

A GM pool deposit is not a fixed-yield position. Depositing mints GM tokens representing a share of the pool (see [/guides/pools](/guides/pools) for the deposit and withdrawal mechanics); that share's value moves with three things happening at once:

- **Net trader profit and loss.** When traders on the market are net profitable, the pool pays their gains out of its own balance — the pool is the counterparty, not a matched order book. When traders are net losing, those losses accrue to the pool as a gain.
- **The long token's price.** The pool holds a real balance of the long token (`longTokenAmount` in `PoolValueInfo`, from `SyntheticsReaderClient.getMarketPoolValueInfo`), so a depositor carries that token's price exposure directly, on top of any trader PnL.
- **Fee and funding income.** Position fees, borrow fees, and net funding paid by traders (see [/concepts/funding-and-fees](/concepts/funding-and-fees)) flow toward the pool as `totalBorrowingFees` and related fields in the same `PoolValueInfo` response.

Stated without euphemism: if traders on a market are heavily net long into a sustained price rise, the pool is functionally short that token over the same period and loses value as the price rises, regardless of how the long token's own price performs in isolation. If traders are heavily net short into a decline, the pool loses in the opposite direction. A depositor who wants exposure to a token's price without also taking the other side of that market's trader positioning is holding the wrong instrument.

## How pool composition limits leverage and price impact

The pool's current balance directly caps how much size the market can support on each side. `useMarketsInfo.ts` computes the liquidity available to new long or short size as the pool's total USD value minus the open interest already outstanding on that side:

```
availableLiquidityLong = max(poolValueUsd - openInterestLong, 0)
availableLiquidityShort = max(poolValueUsd - openInterestShort, 0)
```

Both `poolValueUsd` (from `getMarketPoolValueInfo`) and `openInterestLong` / `openInterestShort` (from `getOpenInterest`) are read from `SyntheticsReaderClient` per market. As open interest on one side grows toward the pool's value, the liquidity left for more size on that side shrinks toward zero — a pool with heavy long skew has little room left for new longs even if the pool itself is large, because the same balance is already backing the existing long exposure. `maxLeverage` is currently a fixed `50` in `useMarketsInfo.ts` rather than a value read from `DataStore`, so it does not yet reflect this constraint per market.

Price impact (see [/concepts/funding-and-fees](/concepts/funding-and-fees)) is meant to widen as a trade pushes a market further toward one-sided open interest, discouraging trades that would leave the pool more exposed. The current client applies a flat rate regardless of pool composition; that gap is documented on the funding-and-fees page rather than repeated here.

## Where this lives in the contracts

`SyntheticsReader` is the read path for all of this: `get_market_pool_value_info` for the pool's balances and PnL, and `get_open_interest` for the outstanding long and short exposure per market — see [/reference/synthetics-reader](/reference/synthetics-reader) for the full field list and units. Trades that change pool exposure enter through `ExchangeRouter`, described in [/reference/exchange-router](/reference/exchange-router).

```text
Trader submits an order
ExchangeRouter.create_order() ──▶ OrderVault holds collateral
Keeper executes the order
Position opens or changes size against the market's one pool
├──▶ openInterestLong / openInterestShort shift (get_open_interest)
Pool's long-token and TUSDC balance, and its PnL, shift with the trade
(get_market_pool_value_info) ──▶ every depositor's GM share revalues
```

Caption: one order changes the same pool that every depositor's GM share is priced against — there is no separate ledger per side or per depositor.

## Worked example

A BTC/USD pool is worth $500,000 and currently carries $180,000 of long open interest and $60,000 of short open interest. Available liquidity for new longs is `500,000 − 180,000 = $320,000`; for new shorts it is `500,000 − 60,000 = $440,000`. A trader then opens a $250,000 long. Long open interest becomes `180,000 + 250,000 = $430,000`, and available liquidity for further longs drops to `500,000 − 430,000 = $70,000` — even though the pool's own value has not changed, the room left for the same side has shrunk by more than the new position's size relative to what remained, because the new long consumed most of what was available. A depositor who joined before this trade is now backing a pool with a much larger, and much more one-sided, long exposure than when they deposited.

## Related

[/guides/pools](/guides/pools) covers adding and removing liquidity and the deposit worked example. [/concepts/funding-and-fees](/concepts/funding-and-fees) covers the fee and price-impact side of the same mechanism. [/concepts/risk](/concepts/risk) is the full list of what can go wrong on the depositor side.
2 changes: 1 addition & 1 deletion apps/docs/content/developers/local-setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ bun run --cwd apps/web typecheck
bun run --cwd apps/web build
```

**Key rule: run the whole gate, not a subset.** Even if you only changed a comment, run all commands. Turbo caches aggressively, so the full run is usually just a few seconds.
**Key rule: run the whole gate, not a subset.** Even if you only changed a comment, run all commands. Turbo caches aggressively, so the full run usually takes a few seconds.

The gate exists to catch:

Expand Down
Loading
Loading