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
89 changes: 35 additions & 54 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,73 +34,54 @@ env:
# so both need multisig's wasm built first too — that's also why the
# multisig build step above runs before the vault build step, not just
# before options_market's.
#
crate: [options_market, price_oracle, vault, multisig, streams, staking, timelock, governor, lp_token, lp_pool, integration_tests]
jobs:
fmt:
runs-on: ubuntu-latest
strategy:
matrix:
crate: [options_market, price_oracle, vault, multisig, common, streams, staking, timelock, governor, params, grants_escrow, lp_token, lp_pool, integration_tests, tools/spec-diff, tools/resource-sna
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo xtask fmt --check

clippy:
# Workspace-wide formatting and lint gate (issue #226).
#
# Runs `cargo fmt --all -- --check` (honouring the repo's rustfmt.toml)
# and `cargo clippy --workspace --all-targets -- -D warnings` as two
# distinct steps so a failure is attributable to exactly one of them.
# The toolchain is pinned to the same `dtolnay/rust-toolchain@stable`
# (with rustfmt, clippy and wasm32-unknown-unknown) used by every other
# job in this file.
#
# ADVISORY FLAP: while the four contracts are being reconstructed and
# stub-heavy crates are still being filled in, this job may land
# advisory-only via `continue-on-error: true` below. Flipping it to
# blocking is a one-line change: delete that flag. Do not leave it
# advisory indefinitely — it must be removed once the workspace is
# lint-clean so failures block merge.
#
# Pre-existing lints that cannot be fixed immediately must be captured
# as narrowly scoped, commented `#[allow(...)]` attributes at the item
# level (never a crate- or workspace-wide allow), each referencing a
# tracked follow-up issue. `#[contractimpl]`-generated code in the
# wasm-oriented contract crates is the usual source of these.
lint:
runs-on: ubuntu-latest
strategy:
matrix:
crate: [options_market, price_oracle, vault, multisig, common, streams, staking, timelock, governor, params, grants_escrow, lp_token, lp_pool, integration_tests, tools/spec-diff, tools/resource-snapshot]
# Remove this line to make the job blocking (see ADVISORY FLAP above).
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
components: rustfmt, clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo xtask check-imports
- run: cargo xtask clippy
- name: cargo fmt --all -- --check
run: cargo fmt --all -- --check
# options_market (and price_oracle/vault) contractimport! other
# contracts' wasm, so that wasm must exist before clippy can even
# compile the workspace. xtask builds it in dependency order.
- name: Build cross-contract wasm dependencies
run: cargo xtask check-imports
- name: cargo clippy --workspace --all-targets -- -D warnings
run: cargo clippy --workspace --all-targets -- -D warnings

test:
runs-on: ubuntu-latest
strategy:
matrix:
crate: [options_market, price_oracle, vault, multisig, common, streams, staking, timelock, governor, params, grants_escrow, lp_token, lp_pool, integration_tests, tools/resource-snapshot]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check
working-directory: ${{ matrix.crate }}

wasm:
runs-on: ubuntu-latest
strategy:
matrix:
crate: [options_market, price_oracle, vault, multisig, streams, staking, timelock, governor, params, grants_escrow, lp_token, lp_pool, integration_tests]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test

wasm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo xtask build
# Fails if soroban-sdk/testutils
crate: [options_market, price_oracle, vault, multisig, common, streams, staking, timelock, governor, params, grants_escrow, lp_token, lp_pool, integration_tests, tools/spec-diff, tools/resource-snapshot]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand Down
72 changes: 61 additions & 11 deletions docs/lp_pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,31 @@ epoch N+1 open ──► users: claim(user) → shares / assets from epoch N

| Function | Who | What |
|---|---|---|
| `initialize(admin, keeper, asset, market, max_utilization_bps)` | admin | One-time setup. |
| `initialize(admin, keeper, asset, market, lp_token, max_utilization_bps)` | admin | One-time setup. Pins the `lp_token` contract the pool mints/burns through. |
| `set_keeper`, `set_max_utilization`, `set_series_cap(series_id, max_collateral)` | admin | Governance risk limits. A cap of 0 de-lists a series. |
| `deposit(user, amount)` | user | Queues `amount ≥ MIN_DEPOSIT` into the current epoch. |
| `request_withdraw(user, shares)` | user | Moves shares out of the balance into a withdrawal ticket for the current epoch. |
| `claim(user)` | anyone | Settles `user`'s tickets from processed epochs (shares credited, assets paid to `user`). `deposit`/`request_withdraw` claim first automatically. |
| `request_deposit(user, amount)` | user | Queues `amount ≥ MIN_DEPOSIT` into the current epoch's deposit queue. No shares are minted yet. |
| `request_withdraw(user, shares)` | user | Burns nothing yet: moves `shares` out of `user`'s balance into a withdrawal ticket for the current epoch. |
| `claim(user)` | anyone | Settles `user`'s tickets from finalized epochs: mints shares for deposits, pays assets for withdrawals. `request_deposit`/`request_withdraw` claim first automatically. |
| `write_option(series_id, contracts, collateral, min_premium)` | keeper | Writes on `options_market` with the pool as writer. `collateral` must equal the market's required collateral: the pool authorizes a transfer of exactly that amount. |
| `reclaim(position_id)` | anyone | Pulls a settled position's collateral back from the market. |
| `set_expected_liabilities(amount)` | keeper | Mid-epoch mark-to-market (informational). |
| `process_epoch()` | keeper | Fixes the epoch price and converts every queued ticket. |
| `process_epoch()` | keeper | Finalizes the current epoch: fixes the share price and converts every queued ticket. Requires zero open positions. |
| `share_price()` | anyone | Read-only. Current share price (see below). |
| `pending_deposit(user)` | anyone | Read-only. `user`'s queued deposit amount in the current epoch. |
| `pending_withdraw(user)` | anyone | Read-only. `user`'s queued withdrawal shares in the current epoch. |
| `pending_epoch()` | anyone | Read-only. The epoch index that is currently open for requests. |

## Epoch pricing rule

Each epoch has one deposit queue and one withdrawal queue. Requests made
during epoch `N` are **only** priced when epoch `N` is finalized by
`process_epoch`. A request can never be filled at the price it was
observed at: the price used is the one fixed at finalization, after all
open positions in the epoch have settled and been reclaimed. Re-requesting
in the same epoch adds to the existing ticket rather than creating a new
one, and `claim` can be called at any time after finalization — including
several epochs later — and always settles at the price fixed for the
epoch the ticket was queued in.

## Share price and mark-to-market

Expand All @@ -34,8 +50,11 @@ share_price = (total_assets + VIRTUAL_ASSETS) / (total_shares + VIRTUAL_SHARES)
```

- **idle** is the USDC the pool owns that isn't locked. It's tracked
internally and never read from the token balance.
- **locked** is the collateral held in open short positions.
internally and never read from the token balance of the pool address;
donations to the pool address do not change `idle`.
- **locked** is the collateral held in open short positions. It is
excluded from the withdrawable base: withdrawals can only be filled
from assets that are not locked against a live series.
- **Accrued premium**: `options_market` pays the writer's premium up front
in `write_option`, so it is already part of `idle` when the option is
written.
Expand All @@ -50,7 +69,39 @@ conversion time `locked = 0` and every liability is **realized**:
`reclaim` credited exactly what the market returned. A loss larger than
the premium therefore lowers `idle`, which lowers the share price, and
every holder in that epoch shares it pro rata. Both conversions round
down, in the pool's favour.
down, in the pool's favour; any rounding dust on share burn stays in the
pool and accrues to remaining holders.

## Shares and `lp_token`

Shares are SEP-41 balances held in the pinned `lp_token` contract. The
pool is the **only** minter and burner:

- `process_epoch` mints shares to the pool's own account for the epoch's
deposit queue, then `claim` transfers them to each depositor.
- `request_withdraw` moves shares from the user into the pool's escrow
account inside `lp_token`; `process_epoch` burns them once the epoch's
price is fixed and `claim` pays out the corresponding assets.

Because minting happens only at finalization, the share price cannot be
moved by a same-ledger deposit/withdraw round-trip.

## Collateral deployment and `vault` tagging

Pool capital is deployed through `options_market`'s writer flow, never by
transferring directly to the market. Every `write_option` call:

1. Checks the series whitelist/cap and utilization limits against `idle`.
2. Authorizes `options_market` to pull exactly `collateral` from the pool.
3. Records the resulting position with the `vault` tag reserved for the
pool, so the collateral is accounted separately from individual
writers' collateral. Pool funds are never commingled with individual
writers' collateral, and `reclaim` returns collateral to the pool's
`idle` only.

While a position is open its collateral counts as `locked` and cannot
back a withdrawal; a withdrawal queued in that epoch is only filled after
the series settles and `reclaim` has returned the collateral.

## Risk limits

Expand Down Expand Up @@ -92,6 +143,5 @@ down, in the pool's favour.
returning collateral in `reclaim_collateral`. If one position can't
settle, `process_epoch` is blocked, and with it all deposits and
withdrawals.
- **Shares aren't a token yet.** Shares are internal balances. The
SEP-41 `lp_token` contract exists for composability; wiring the pool
to mint through it is a follow-up.
- **No secondary market.** Shares are transferable SEP-41 balances via
`lp_token`, but the pool does not operate a market for them.
Loading
Loading