# Draft Solana programs These are example programs, written to be read. **Nothing here is deployed.** There is no program ID on any cluster, no keypair, no RPC secret, and no funded account in this tree. The live prototype at `http://localhost:8080` settles on simulated balances and does not touch a chain. These drafts exist so the economics can be reviewed as code, and so a devnet test is a short step rather than a rewrite. Two small programs, each with one job: | Program | Directory | What it does | | --- | --- | --- | | `science_market` | `[internal location withheld]` | Market and position accounts. Initialize a market with the proposition pinned, open a fully funded position, settle it at a score, and enforce the conservation invariant. | | `science_dealer` | `[internal location withheld]` | The quote rule, the per-leg fee, and the dealer's reserve accounting. Committed reserve cannot be withdrawn. | The split is deliberate. The core program is about the contract; the dealer program is about the money the maker posts and the fee. Keeping them apart makes each one small enough to read, and puts the one guardrail the legal brief names — a maker must not be able to withdraw committed collateral — in a program that has nothing else to look at. ## Builder's note on the toolchain `cargo`, `solana`, and `anchor` are all present, and every check below has been run in this tree: ``` cargo check # both programs compile: no errors cargo test # 20 unit tests pass anchor build # builds both programs and the IDL anchor test # 15 integration tests pass on a local validator ``` `anchor build` needed a lock-file pin (see `SECURITY.md`): `anchor-syn 0.30.1` calls `Span::source_file()`, which `proc-macro2` >= 1.0.80 removed, so `Cargo.lock` is pinned to older transitive crates for the IDL step. That pin does not change the program logic, but it does mean the artifact hash depends on the lock file. Beyond the tests, the drafts have been through Miri, a Trident fuzzing run, a Kani model-check of the pure math, `cargo audit`, `cargo deny`, `cargo geiger`, and `cargo udeps`. Soteria was attempted and could not compile the Agave line inside its container. Everything is local: no devnet or testnet run and no third-party audit. See `SECURITY.md` for exactly what ran, what each tool found, and what did not run. ## Build and test ```bash cd programs # Rust: compile-check and run the unit tests. No validator, no Anchor CLI needed. cargo check cargo test # Anchor: build the programs and the IDL, then run the integration suite. # Requires the Anchor CLI, a Solana keypair in the default path, and a local # validator that `anchor test` starts for you. anchor build anchor test # If you have never installed Anchor: cargo install --git https://github.com/coral-xyz/anchor avm --locked avm install 0.30.1 && avm use 0.30.1 ``` `anchor test` runs `[internal location withheld]` against a local validator. The tests need the IDL, so `anchor build` must run first. The unit tests cover the arithmetic only: the leg split, the payout split, the fee on dollars, the break-even identity, and the quote clamp. They have no accounts and need no cluster. The integration tests cover the same numbers through real instructions, plus the refusals that matter: a second settlement of a closed position, an opening with no eligibility account, and an opening whose trader leg rounds to zero. ## The economics, as implemented The published terms are the source of truth. This is the same thing in code: ``` N maximum gross payout of a position p entry score, in (0,1) q settlement score, in (0,1) f fee rate per leg UP trader posts p*N, dealer reserves (1-p)*N DOWN trader posts (1-p)*N, dealer reserves p*N settle trader gross = q*N (UP) or (1-q)*N (DOWN); the dealer gets the rest fees entry fee on the trader's contribution close fee on the trader's gross proceeds both to the fee vault; the dealer's reserve is never charged ``` Scores are basis points: an integer in `1..=9999`, meaning `0.0001..=0.9999`. Both endpoints are excluded. A score of 0 or 1 has no two-sided price and would hand the whole pot to one side, so the program refuses it rather than pretending it is a probability. Amounts are the collateral token's base units. At 6 decimals, `100_000_000` is 100 USDC. ## The account model Five accounts. No global registry, no per-position vault, no metadata program. | Account | Seeds | Holds | | --- | --- | --- | | `Market` | `["market", authority, market_id]` | The frozen claim, the settlement mint, the fee vault address, `reserve_locked`, `open_positions`, `paused`. | | `Position` | `["position", market, trader, side, entry_score, fee_bps, contribution]` | One fully funded bilateral contract: both legs, N, the fee copied at open, and the outcome fields. | | vault (token) | `["vault", market]` | All collateral for the market, held by the market PDA. One vault per market, not one per position. | | `Eligibility` | `["eligibility", market, trader]` | A boolean, an expiry, and the trader's key, scoped to one market. Nothing else. | | `Dealer` | `["dealer", authority]` | The fee rate, the quote parameters, `reserve_balance`, and `committed`. | `Position` is seeded on the market, trader, side, entry score, fee basis points, and contribution, so a trader can hold several positions on one market and each is a distinct account. There is no instruction anywhere in either program that rewrites `Market.claim`. Changing the wording means a new market, which is what the published methodology promises. The vault is owned by the market PDA. The dealer's reserve account is owned by the dealer PDA. Neither is owned by a person, which is what makes "committed collateral cannot be withdrawn" a property of the program rather than a promise about an operator's behaviour. ## The invariant One invariant carries the contract: **for every position, `trader_contribution + dealer_contribution == max_payout`**, and the collateral in the vault covers every open position's payout. It is enforced in three places: 1. `open_position` computes the trader leg from N, then subtracts to get the dealer leg, and asserts the two sum to N before any signature is used. 2. `settle_position` asserts `trader_payout + close_fee + dealer_payout == max_payout` before the money moves. This is an identity given the lines above it, and it is checked anyway so a later edit cannot quietly break it. 3. `settle_position` requires `vault.amount >= market.reserve_locked` before paying anyone. Since `reserve_locked` includes this position's own reserve, paying this position out cannot dip into another one's. The second invariant is about time: **a position settles once**. `settle_position` requires `status == Open` and writes `Closed` in the same instruction. The test suite asserts that a second call fails. ## How the fee basis is computed, and where it is set A fee is a percentage of dollars, per leg: once on the consideration the trader pays to open, and again on the gross proceeds the trader receives on close. It is never a percentage of the maximum payout N. `fee_on(amount, fee_bps)` is the only place either program computes a fee, and it is `amount * fee_bps / 10_000`, rounded down. Every call site passes a dollar figure — a contribution or a gross proceed. No call site passes `max_payout`. If someone later tries to charge on N, they have to write a new line, not change a flag. The rate itself is set in one of two places depending on the leg: - **At open**, the trader passes `fee_bps` with the instruction, and the program caps it at `MAX_FEE_BPS` (10%). The value is copied onto the position, so a later change cannot reprice an existing contract. - **At close**, the program reads `position.fee_bps`. There is no way to set it after the position exists. The published default is 2% per leg. On the brief's own illustration — a 100-USDC UP position entered at 0.40 and closed at 0.72 — the trader posts 40.00 plus a 0.80 entry fee, receives 70.56 at close after a 1.44 close fee, and the dealer receives 28.00. Both unit and integration tests pin those exact numbers, so the code and the disclosure page can be diffed against each other by eye. The break-even identity from the terms, `p * (1 + f) / (1 - f)`, is in `break_even_score_millis`, with a unit test against the published example: 0.50 at 5% per leg breaks even at 0.5526. ## The on-chain eligibility check This is the minimal on-chain half of a control whose policy lives off chain. The legal brief (§7) is blunt about why it has to exist: a rule that exists only on the website is not an effective program-level restriction, because nothing stops someone calling the program through their own RPC endpoint. What it does: - `open_position` requires an `Eligibility` account, seeded on the trader's key. The account must have `allowed == true`, and its `expires_at` must be in the future (or zero for no expiry). - `set_eligibility` writes that account, and only the market authority can call it. - Without the account the opening fails. With `allowed == false` it fails. A caller cannot borrow someone else's credential, because the PDA includes their key. What it deliberately does not do: - **No identity is stored on chain.** The account holds a key, a boolean, and a timestamp. No name, no address, no country, no document. The jurisdiction, sanctions, terms, and age checks stay in the operator's own systems, where the data already is. - **It is not a sanctions program, and it is not KYC.** It is a switch. A real deployment drives it from a screening vendor and a counsel-approved matrix, and keeps a human in the loop. - **It does not touch an open position.** Revoking a credential stops new openings. It does not force a close, and it does not block settlement. The one thing that can hold a payout is `restrict_owner`, and that returns the trader's own contribution and freezes the rest rather than taking it. - **It cannot make the policy correct.** A wrong allowlist entry written by the authority is a wrong payment, and the program will not notice. ## What is not handled on chain Said plainly, because the gap is the interesting part: - **Identity and KYC.** Nothing verifies who a wallet belongs to. - **Sanctions screening.** No vendor feed, no OFAC list, no escalation path. A boolean is not a program. - **Jurisdiction and product permissions.** Off chain, in the Django app's compliance layer. - **Disputes.** No challenge window, no appeal, no independent decision-maker. The brief (§9) requires all three before a real listing. - **Oracle integrity.** The settlement authority's signature in this draft only says who submitted the score. It does not say the score is right, well-sourced, or timely. A real design publishes the score off chain first, commits it, and treats the on-chain submission as one step in a documented sequence. - **Records and reporting.** Events are emitted; the audit trail, reconciliation with the legal books, and the reports a regulator would want are not here. - **Upgrade authority.** These programs are upgradeable by their deployer as written. That is fine for devnet and unacceptable for a public financial program without a stated policy. - **Stablecoin risk.** See below. ## The denomination assumption Collateral is a token, and the intended one is USDC. Full funding in USDC units is not full funding in dollars. If USDC trades below a dollar, or its issuer freezes an account, or redemption is slow, a position that was fully funded in units can fall short in value. The program does not model any of that and does not convert anything. It holds one mint and pays out in that mint. The published terms say the same thing (§6), and a deployment has to decide whether obligations are USDC-denominated or dollar-denominated and disclose which. This draft assumes the former. It does not pretend the two are the same. ## Before devnet What has to happen before these could reasonably be tested on devnet, in order: 1. **Run the suite on devnet.** The integration suite passes on a local validator; it has not run against a real cluster. Expect to fix seed encoding and account ordering on the first pass. 2. **Write an IDL-level test for the invariant under adversarial ordering**: a settlement that tries to use another market's vault, a payout routed to the wrong token account, a clamped score at both ends, and a position opened by a dealer with an empty reserve account. 3. **Decide the oracle path.** A single authority signer is fine for a test and wrong for anything else. The score has to be committed off chain, with the failure and stale-round rules the methodology page describes, before the settlement instruction is trusted. 4. **Pin the mint.** `initialize_market` accepts any mint. A deployment pins the reviewed USDC mint and refuses everything else. 5. **Test on devnet with worthless tokens first.** A locally minted mock with 6 decimals behaves the same in the code and costs nothing when the arithmetic is wrong. 6. **Set the upgrade policy before the program ID is public.** An upgradeable program with a single hot key is a different product from an immutable one, and the brief's first recommendation is not to deploy the immutable version before classification is resolved. 7. **Keep the fee schedule and the site in step.** The rate lives in the instructions and the disclosure lives on the site; a test that asserts the two agree is worth more than either. None of this is a substitute for the legal work. The brief's first recommendation is to build this as infrastructure for a regulated business and to resolve classification before an immutable public deployment, not after. These drafts are the infrastructure half.