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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ tests/keys/
# Lives in the private audit repo instead.
AUDIT_SCOPE.md
AUDIT_PACKAGES.md
# Same two files after the 2026-09-01 archive move — they carry audit pricing
# and vendor relationships and must never reach the public repo.
docs/archive/AUDIT_SCOPE.md
docs/archive/AUDIT_PACKAGES.md
63 changes: 57 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,21 @@ the launch figure, so it silently goes wrong every time the launch figure moves.

### Program layout (`programs/soladrome/src/`)

⚠️ This table described a flat tree until 2026-09-01 and was wrong from the 2026-08-30
restructure onward: there is no top-level `state.rs`, `amm.rs` or `amm_state.rs` any more.
Handlers and their `#[derive(Accounts)]` contexts live together under `instructions/`, and the
account structs under `state/`.

| File | Role |
|---|---|
| `lib.rs` | All instruction entry points + every `#[derive(Accounts)]` context |
| `state.rs` | On-chain account structs: `ProtocolState`, `UserPosition`, bribe/gauge PDAs |
| `math.rs` | Bonding curve math: `sola_out()`, `advance_accumulator()`, `pending_fees()` |
| `lib.rs` | **Dispatch only** — one wrapper per instruction plus `declare_id!`. No logic, no contexts |
| `constants.rs` | Seeds, allocation sizes, caps, fee rates |
| `errors.rs` | `SoladromeError` enum |
| `amm.rs` | AMM instruction logic + account contexts (`CreatePool`, `AddLiquidity`, `RemoveLiquidity`, `Swap`) |
| `amm_state.rs` | `AmmPool` struct, `sort_mints()` |
| `math.rs` | Bonding curve math: `sola_out()`, `advance_accumulator()`, `pending_fees()` |
| `amm_math.rs` | `swap_out()`, `lp_for_deposit()`, `tokens_for_lp()`, `isqrt()`, `MINIMUM_LIQUIDITY` |
| `token_ext.rs` | **Token-2022 admission** — which mint extensions are refused, and why |
| `instructions/*.rs` | One file per domain: `admin`, `amm`, `borrow`, `bribes`, `curve`, `emissions`, `gauges`, `partners`, `pol`, `stake`, `ve`, `vesting`. Each owns both its handlers and its account contexts |
| `state/*.rs` | The 22 on-chain account types, incl. `AmmPool` and `sort_mints()` |

### Two separate systems share one codebase

Expand All @@ -169,6 +175,49 @@ the launch figure, so it silently goes wrong every time the launch figure moves.
- First LP deposit locks `MINIMUM_LIQUIDITY = 1_000` to `LP_DEAD_PUBKEY` (System Program)
- `lp_for_deposit()` auto-rebalances to the limiting token side on subsequent deposits

### ☢️ Token-2022 — supported since 2026-09-01, and NOT bounded to the AMM

**A third-party mint enters the program in exactly three places**, and the second was missed on
the first reading of this migration:

1. `amm.rs` — a pool's two mints.
2. `bribes.rs` — **`reward_mint` is arbitrary**, so a partner bribing in USDG never touches the
AMM at all.
3. `partners.rs` — `bribe_mint` / `reward_mint` on the escrowed bribe stream.

Everywhere else the mint is SOLA, oSOLA, USDC or a protocol LP mint. Curve, floor, staking,
borrow and ve never see a Token-2022 mint.

**A pool carries TWO token programs.** Its sides may be served by different ones — an xStock
(Token-2022) quoted in USDC (classic SPL) is the shape the feature exists for — so
`create_pool`, `add_liquidity`, `remove_liquidity` and `amm_swap` each take `token_a_program`
and `token_b_program`, each bound to its mint by `mint::token_program`. Collapsing them into one
would refuse the only pair shape worth having. `crank_partner_epoch` carries two for a different
reason: it moves a bribe tranche (possibly T22) and mints SOLA (always SPL) in one instruction.

**The protocol's own mints stay classic SPL Token** — SOLA, oSOLA and every pool's LP mint,
through the separate `token_program` account. Keep it that way: it is what leaves wallets, ATAs
and every LP integration untouched, and confines the interface surface to mints we do not
control.

**`token_ext::require_supported_mint` is the whole admission policy.** Refuses a transfer fee
(the vault would receive less than the amount booked into `reserve_a` / `total_bribed`, silently
and cumulatively), an **armed** transfer hook (transfers need accounts we do not pass, so
`remove_liquidity` would revert and lock LP funds), and `DefaultAccountState::Frozen` (vault born
unable to move). Deliberately **allows** a permanent delegate, a pausable config and a scaled UI
amount — refusing those would exclude the xStocks, which is the entire point.

⚠️ The gate is at **admission**, never at transfer time. Pool and bribe-vault seeds are `init`,
so a mint found bad after the accounts exist leaves a residue that can never be cleared and the
pair becomes permanently unopenable — the same shape as the July 2026 devnet brick.

☢️ **Residual risk, disclosed and not closable here:** the xStocks' hook slot is unarmed today
and armable at any time. We refuse an already-armed mint; we cannot refuse one armed later.

⚠️ **Off-chain caution:** `ScaledUiAmountConfig` is harmless on-chain (the AMM works in base
units) but any pricing or points code that reads decimals without the scale factor is wrong by
the split ratio.

**Gauge / Bribe system**
- 7-day epochs (`EPOCH_DURATION = 604_800 s`); `current_epoch = unix_ts / EPOCH_DURATION`
- **Who gets paid what** (code-verified 2026-07-17, a recurring point of confusion):
Expand Down Expand Up @@ -399,7 +448,9 @@ renewal path, the migration path for the old 160-byte layout, and the only way o
second bag.

**`partner_deposit_bribe` was deleted** — without the match it was `deposit_bribe` renamed.
**57 instructions** (−1 for that deletion, +1 for `close_legacy_partner_allocation`). Seventeen
**57 instructions at the time** (−1 for that deletion, +1 for `close_legacy_partner_allocation`).
⚠️ Since then the 2026-08-30 restructure took it to 53 and `recycle_lp_emissions` brought it to
**54** — see STATUS.md, which is the file to trust for counts. Seventeen
`[partner]`/`[crank]`/`[close]`/`[stream]` cases in `tests/bankrun_allocations.ts`.

⚠️ **`PartnerAllocation` grew 160 → 192, and that needed an escape hatch.** `register_partner`
Expand Down
2 changes: 1 addition & 1 deletion MAINNET_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

| # | Item | Status | Notes |
|---|---|---|---|
| 1 | **Security audit — FULL SCOPE** | ⏳ Open — **scope decided 2026-08-04** | Sec3 quote in hand (~$37K); Accretion.xyz in the running (warmer relationship + Marinade intro). Blocks: mainnet deploy, Jupiter routing/listing, any external volume. **DECISION 2026-08-04: do ONE full-scope audit — the split gated/delta packaging is abandoned.** Rationale: splitting *raised* the total (~+10-20%, a delta re-audit carries a fixed re-familiarization cost) while complicating the mainnet deploy, and the reduced package left `emissions_enabled = false`, i.e. **no LP incentive at launch → empty pools → nothing for Jupiter to route → no fees**. The full audit is the *enabling* purchase, not the expensive option: it is what lets emissions be on at launch. Scope reasoning kept in [AUDIT_PACKAGES.md](AUDIT_PACKAGES.md) / [AUDIT_SCOPE.md](AUDIT_SCOPE.md) (both now historical). **Phase flags stay — but for launch sequencing only (§3b), never again as an audit-scope-reduction device.** See [[project-soladrome-funding-gtm]]. |
| 1 | **Security audit — FULL SCOPE** | ⏳ Open — **scope decided 2026-08-04** | Sec3 quote in hand (~$37K); Accretion.xyz in the running (warmer relationship + Marinade intro). Blocks: mainnet deploy, Jupiter routing/listing, any external volume. **DECISION 2026-08-04: do ONE full-scope audit — the split gated/delta packaging is abandoned.** Rationale: splitting *raised* the total (~+10-20%, a delta re-audit carries a fixed re-familiarization cost) while complicating the mainnet deploy, and the reduced package left `emissions_enabled = false`, i.e. **no LP incentive at launch → empty pools → nothing for Jupiter to route → no fees**. The full audit is the *enabling* purchase, not the expensive option: it is what lets emissions be on at launch. Scope reasoning kept in `docs/archive/AUDIT_PACKAGES.md` / `docs/archive/AUDIT_SCOPE.md` (both historical, and both deliberately outside this public repository). **Phase flags stay — but for launch sequencing only (§3b), never again as an audit-scope-reduction device.** See [[project-soladrome-funding-gtm]]. |
| 2 | **`deploy_pol` rewrite for jitoSOL leg** | ⏳ Open | Currently hardcoded to SOLA/USDC (`pol.rs`). Needed before the SOLA/jitoSOL house pool can be POL-funded. Blocks: house pool liquidity, Jupiter routing (nothing worth routing to without it). |
| 3 | **Jupiter `Amm` adapter** | ⏳ Design only — **priority raised 2026-08-04** | See [JUPITER_ADAPTER_DESIGN.md](JUPITER_ADAPTER_DESIGN.md). Depends on #1 and #2. Not started in code. **Founder decision: move this EARLIER in the roadmap** — indexing the ecosystem AMM pools on Jupiter from day one routes external swap volume through them, and every routed swap pays the protocol fee into `market_vault` → hiSOLA stakers. Maximizing fees from launch is the goal. ⚠️ Scope note to settle when we resume: this is about the **ecosystem pools (LST/stable/partner)**, NOT SOLA — SOLA stays out of Jupiter routing (no SOLA pool, §4). Open question kept from §4: these pools are shallow vs incumbent Raydium/Orca LST pools, so weigh expected routed volume before spending the adapter effort. Decision is to prioritize; the volume question is to be answered, not ignored. |

Expand Down
113 changes: 113 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# STATUS — where Soladrome actually is

One living document. If something here disagrees with another file in this repository, this
file is the one to trust, and the other file is the one to fix.

**Last measured: 2026-09-01.** Every figure below was read from the tree or the chain on that
date, not carried forward from a previous note.

---

## The artefact

| | |
|---|---|
| Current tag | **`audit-2026-09-01`** — the tree handed to the auditor |
| Previous tag | `audit-2026-08-30b`, a verified **ancestor** of the current one |
| Branch | `main` — one trunk, and the deployed tree |
| Program id (devnet) | `DgD37Vjs8ozzBwZnfsNEDQNw1SEsgBTr2TXfBdsrgXpe` |
| Instructions | 54 |
| Account parameters | 503 |
| Error variants | 58 |
| On-chain account types | 22 |
| Tests | **112 passing, 0 failing** |

There is **one binary**. Devnet and mainnet run the identical artefact; the `devnet` cargo
feature was removed on 2026-08-23 and must never come back. See CLAUDE.md for the full story of
why a build-time cluster flag was a security problem rather than a convenience.

## Branches, and what each is for

| Branch | Role |
|---|---|
| `main` | The trunk. Everything ships from here, and a push deploys the frontend to production. |
| `devnet-legacy` | The four account-layout migrations plus their tests. ⛔ **Never merge into `main`** — those migrations are devnet-only and are deliberately outside the audited binary. |
| `chore/cargo-fmt` | Held open on purpose. |
| `feat/vote-escrow-pda` | Research, not a candidate for merge. |

## Continuous integration

CI runs `cargo fmt --check`, `clippy -D warnings`, `anchor build`, the Rust unit tests, **the
bankrun suite**, **the validator integration suite**, and the frontend type-check and build.

The two test jobs were added on 2026-08-31. Before that date CI was green without running a
single one of the 112 cases, so a pull request that broke the whole suite passed. Worth
remembering when reading any test claim made in a document written before then.

⚠️ On **Node 22.18–23.x** the suites need `NODE_OPTIONS=--no-experimental-strip-types`. Native
TypeScript type-stripping claims the `.ts` file before ts-node's require hook, serves it as ESM,
and the run dies on `SyntaxError: Named export 'BN' not found` — `@coral-xyz/anchor` is
CommonJS. Node 24 resolves it the other way, so identical code is green on 24 and red on 22.

## Recently landed

**Token-2022 support (2026-09-01).** Third-party mints are accepted across the three surfaces
that take one: `amm.rs`, `bribes.rs` and `partners.rs`. A pool carries two token programs, since
its sides may be served by different ones. Admission policy lives in one file, `token_ext.rs`.
The protocol's own mints — SOLA, oSOLA, every LP mint — stay classic SPL Token.

☢️ **The residual risk that is not closable in code:** the xStocks ship with an *unarmed*
transfer-hook slot the issuer may arm at any time. A mint that is already armed is refused; one
armed *after* its pool exists would make that pool's transfers fail, `remove_liquidity`
included. Disclosed to the auditor, not solved.

**`recycle_lp_emissions` (2026-09-01).** An unclaimed LP emission pot was never minted at all —
a budget leak, not a vulnerability. The residue now rolls forward into the same pool's current
epoch, after the same grace period a bribe rollover waits.

## Open decisions, with no deadline yet

**The licence Change Date is fixed, and that is a decision by default.** `LICENSE` is BUSL-1.1
with a Change Date of **2030-05-13** — an absolute date, not a rolling window. It approaches on
its own: mainnet has not launched, so whatever protection remains shrinks every day without
anyone choosing it. A rolling conversion (N years after each version's first release) is the
alternative worth considering. Out of audit scope — nobody audits a `LICENSE` — so it can change
without contradicting anything already handed over, but it should be an actual decision.

**Should a pool on a pausable Token-2022 mint be gauge-eligible?** If it is, emissions can be
voted toward a market its issuer has frozen. Undecided.

## Subsystems shipped but not enabled

Both are in the audited binary and both are in scope. A runtime flag does not put code out of
scope: a gated instruction is still deployed bytecode, and flipping the flag is one transaction.

- **POL** (`pol.rs`) — protocol-owned liquidity.
- **The per-epoch oSOLA emission cycle** — the gauge-directed pot, distinct from the continuous
per-pool stream.

## Points

**Kept, and switched on after the audit** — the mainnet pre-TGE phase, not dead code.

What is actually deployed today: `app/lib/points.ts`, both `api/points` routes and
`supabase/points_phase2.sql` are on `main` and therefore live. What is **not** built: the cron
that drives accrual, and the Points page in the frontend. So the engine exists and nothing
currently runs it.

## Archived documents

`docs/archive/` holds planning documents that have been superseded and are kept only because
their reasoning explains how the current numbers were arrived at. **Nothing in `docs/archive/`
should be read as a description of the code as it stands** — every surface figure in there
predates the 2026-08-30 restructure.

Two of them are deliberately excluded from this public repository by `.gitignore`, because they
carry commercial detail that does not belong in public. They exist on the maintainer's disk and
in the audit handoff, not here.

## Where the auditor's documents live

Not in this repository. The handoff package is its own repository so the code has exactly one
home and there is no second copy to drift: scope, architecture, threat model, known issues and
testing instructions, all pinned to `audit-2026-09-01`.
Loading