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
2 changes: 1 addition & 1 deletion AMAZON-DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Written for engineers shipping software that holds other people's money, and for

**Part I** builds the fundamentals: Solana's accounts, tokens, PDAs, and time, then financial software as a state machine, the arithmetic of money, offchain truth, the adversary, and operating a program in production. Exploit classes are taught through the incidents that made them famous, among them the $2.6B SPL token-lending rounding bug, Cashio ($52M), Wormhole ($325M), and Mango Markets ($116M).

**Part II** walks ten complete, tested programs end to end: an escrow, an all-or-nothing token fundraiser, a parimutuel betting market, an order book exchange with a critbit matching engine, a constant-product AMM, an oracle-priced proprietary AMM, a lending and borrowing market with per-slot interest and a liquidation engine, an onchain index fund, a fully collateralized options venue, and a perpetual futures venue with funding, margin, and pre-funded solvency. Every chapter gives the account model, the instruction handlers walked as a story, the math in minor units with its rounding directions, the invariants, the failure modes, the tests that catch each one, and the incentives of everyone involved, operator and user alike. Account maps, state machines, and value flows are drawn in one visual language across all ten, so the programs can be read against each other. They have not been explained side by side in one place before.
**Part II** walks ten complete, tested programs end to end: an escrow, an all-or-nothing token fundraiser, a parimutuel betting market, an order book exchange with a critbit matching engine, a constant-product AMM, an oracle-priced proprietary AMM, a lending and borrowing market with per-second interest and a liquidation engine, an onchain index fund, a fully collateralized options venue, and a perpetual futures venue with funding, margin, and pre-funded solvency. Every chapter gives the account model, the instruction handlers walked as a story, the math in minor units with its rounding directions, the invariants, the failure modes, the tests that catch each one, and the incentives of everyone involved, operator and user alike. Account maps, state machines, and value flows are drawn in one visual language across all ten, so the programs can be read against each other. They have not been explained side by side in one place before.

In the tradition of *TCP/IP Illustrated*, the book teaches the layer that does not change. Frameworks churn. The account model has been stable since Solana launched, the arithmetic of money will not change, and neither will the desire of adversaries to take your assets.

Expand Down
4 changes: 2 additions & 2 deletions appendices/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ A recent block's hash, included in every transaction to bound its lifetime. When

### borrow accumulation factor

A single number per reserve that starts at 1.0 and only grows, compounding the borrow rate over elapsed slots. A borrower's live debt is their stored principal multiplied by it, so interest accrues for everyone without any loan being touched. The lending example calls the field `borrow_accumulation_factor`; see the *Lending and Borrowing* chapter.
A single number per reserve that starts at 1.0 and only grows, compounding the borrow rate over elapsed seconds. A borrower's live debt is their stored principal multiplied by it, so interest accrues for everyone without any loan being touched. The lending example calls the field `borrow_accumulation_factor`; see the *Lending and Borrowing* chapter.

### built-in address

Expand Down Expand Up @@ -213,7 +213,7 @@ The last moment an [option](#option) can be exercised. After it the writer recla

### exponentiation by squaring

Computing (1 + r)ⁿ for large n in log₂(n) multiplications instead of n of them, each one checked and floored the same way. How to compound a rate over many elapsed slots in a single update. See [Newton's method (integer square root)](#newtons-method-integer-square-root) for roots, and the *Arithmetic of Money* chapter.
Computing (1 + r)ⁿ for large n in log₂(n) multiplications instead of n of them, each one checked and floored the same way. How to compound a rate over many elapsed seconds in a single update. See [Newton's method (integer square root)](#newtons-method-integer-square-root) for roots, and the *Arithmetic of Money* chapter.

### faucet

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions building-financial-products-in-solana/lending.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ Carol thinks SpaceX is overvalued and wants to profit if it falls. Her trade is

Weeks pass, and the loans accrue interest without a single token moving. The mechanism is the **borrow accumulation factor**.

Each reserve stores one number that starts at 1.0 and only grows. On every `refresh_reserve()`, the program computes the current borrow rate, multiplies it by the slots elapsed, and grows the factor: new factor = old factor × (1 + rate per slot × elapsed slots). Rather than updating every loan, the reserve stores each borrower's `borrowed_principal` and grows the one shared `borrow_accumulation_factor`. A live debt is that principal times the current factor, rounded *up*, because debt rounds against the debtor. No loan is ever visited individually. Tokens move only when people repay or redeem, like the order book's unsettled balances but across time instead of across fills.
Each reserve stores one number that starts at 1.0 and only grows. On every `refresh_reserve()`, the program computes the current borrow rate, multiplies it by the seconds elapsed, and grows the factor: new factor = old factor × (1 + rate per second × elapsed seconds). Rather than updating every loan, the reserve stores each borrower's `borrowed_principal` and grows the one shared `borrow_accumulation_factor`. A live debt is that principal times the current factor, rounded *up*, because debt rounds against the debtor. No loan is ever visited individually. Tokens move only when people repay or redeem, like the order book's unsettled balances but across time instead of across fills.

The rate itself comes from a **kinked utilization curve**. Utilization is borrowed ÷ (available + borrowed). Below the kink at 80% utilization, the rate climbs gently from 2% toward 20% APR; above it, steeply toward 150%. Above the kink, when nearly everything is lent out, the rate spikes, which pushes borrowers to repay and pulls new suppliers in with yield.

Those percentages are annual, and the accrual is per slot, so the conversion divides by a slots-per-year figure the reserve stores as `slots_per_year`. That divisor is the cluster's slot time written as a count, so it is stored in the config rather than in the code: shorten the slot and the same curve charges more per day, with no number in the program changed to say so. The *Time* chapter covers where the current figure comes from.
Those percentages are annual, and the accrual is per second, so the conversion divides by the seconds in a year. The seconds elapsed are the Clock's timestamp minus the one the reserve stored at its last accrual, `last_accrual_timestamp`. Counting seconds rather than slots keeps a borrower's cost to the quoted rate whatever the network's slot length, and the *Time* chapter covers why. Changing the curve with `update_reserve_config()` accrues at the old rates first, so the seconds already elapsed are charged at the rates that applied to them.

Of the interest that accrues, the reserve factor (10%) is set aside in `accumulated_protocol_fees`, floored, in the suppliers' favor; the rest belongs to the suppliers through the rising share price.

Expand Down
10 changes: 5 additions & 5 deletions building-financial-products-in-solana/perpetual-futures.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ Maria creates the pool and is its `authority`; her revenue is the open and close

Financial state on Solana is accounts, and this one transaction created the three the market needs.

The **pool** is a PDA with seeds `"pool"`, the collateral mint, and the oracle feed, so one market exists per collateral-and-feed pair. Its struct, `Pool`, carries the risk parameters set at creation (`max_leverage`, `maintenance_margin_bps`, `open_fee_bps`, `close_fee_bps`, `liquidation_fee_bps`, `max_confidence_bps`, `funding_rate_per_slot`) and the live accounting this chapter keeps returning to:
The **pool** is a PDA with seeds `"pool"`, the collateral mint, and the oracle feed, so one market exists per collateral-and-feed pair. Its struct, `Pool`, carries the risk parameters set at creation (`max_leverage`, `maintenance_margin_bps`, `open_fee_bps`, `close_fee_bps`, `liquidation_fee_bps`, `max_confidence_bps`, `funding_rate_per_second`) and the live accounting this chapter keeps returning to:

- `liquidity`: what the LPs own. It grows with deposits, trader losses, and funding; it shrinks with withdrawals and trader profits.
- `reserved_liquidity`: the portion of `liquidity` currently pledged as the maximum payout of open positions.
- `total_collateral`: trader money held for open positions. It is tracked separately from `liquidity` because it is not the LPs' money.
- `protocol_fees`: Maria's accrued fees, separate again.
- `long_size`, `short_size`, `long_size_scaled`, `short_size_scaled`: per-side open-interest accumulators, which make solvency checkable without iteration.
- `cumulative_funding` and `last_funding_slot`: the funding index.
- `cumulative_funding` and `last_funding_timestamp`: the funding index.

The pool is the mint authority of the **LP share mint** (seeds `"lp_mint"`) and the token authority of the **custody vault** (seeds `"vault"`), a single token account holding LP liquidity, trader collateral, and fees together, and signs for both with its own seeds. The vault mixes custody; the three ledger fields keep the ownership straight, and the rule that makes that safe is the *Token Fundraiser* chapter's: balances are custody, state is truth.

Expand Down Expand Up @@ -75,16 +75,16 @@ Each open creates a **position** PDA with seeds `"position"`, the pool, the owne

## Funding Is a Fee the Crowded Side Pays

A perpetual never expires, so what stops its price drifting away from the asset it tracks? On every venue the answer is **funding**: a periodic payment that makes the popular side pay for its popularity, nudging traders toward balance. This program implements funding in its simplest form: a pool-wide index, `cumulative_funding`, advances by `funding_rate_per_slot` every slot, in whichever direction the open interest leans; whichever side is heavier pays. Each position remembers the index at its entry (`entry_funding`), and settlement charges size × the index movement since entry, at `FUNDING_PRECISION` fixed point. For example, with a rate of 5,000 per slot, a 5,000 USDC position held through 2,000 slots owes 50 USDC.
A perpetual never expires, so what stops its price drifting away from the asset it tracks? On every venue the answer is **funding**: a periodic payment that makes the popular side pay for its popularity, nudging traders toward balance. This program implements funding in its simplest form: a pool-wide index, `cumulative_funding`, advances by `funding_rate_per_second` every second, in whichever direction the open interest leans; whichever side is heavier pays. Each position remembers the index at its entry (`entry_funding`), and settlement charges size × the index movement since entry, at `FUNDING_PRECISION` fixed point. For example, with a rate of 5,000 per second, a 5,000 USDC position held for 2,000 seconds owes 50 USDC.

The example documents two simplifications:

- **The payment goes to the pool**, to the LPs, rather than to the lighter side of traders.
- **The rate is flat**, rather than scaling with how lopsided the book is.

Production funding is proportional to the imbalance and flows between the sides; the mechanism here teaches the accounting structure, an index plus per-position entry snapshots, which is the part that survives into every real implementation. The test `test_funding_charged_to_long` opens a lone long, warps two thousand slots, and asserts the pool collected exactly the funding the index accrued. To keep every other number in this chapter clean, the walkthrough pool sets the funding rate to zero, as the test suite's default market does.
Production funding is proportional to the imbalance and flows between the sides; the mechanism here teaches the accounting structure, an index plus per-position entry snapshots, which is the part that survives into every real implementation. The test `test_funding_charged_to_long` opens a lone long, lets two thousand seconds pass, and asserts the pool collected exactly the funding the index accrued. To keep every other number in this chapter clean, the walkthrough pool sets the funding rate to zero, as the test suite's default market does.

The rate is quoted per slot, so what a position costs per hour follows the cluster's slot time as well as the rate. A pool that outlives a shortening of the slot charges the heavier side more per hour than it was set up to. `set_funding_rate()` lets the pool operator bring it back in line, advancing the index at the old rate before storing the new one so the slots already elapsed are charged at the rate that applied to them. The *Time* chapter covers why any rate counted in slots needs that.
The rate is quoted per second of wall-clock time, and the index stores the Clock's timestamp at its last advance, so what a position costs per hour is the rate alone, whatever the network's slot length. `set_funding_rate()` lets the pool operator change the rate, advancing the index at the old rate before storing the new one so the seconds already elapsed are charged at the rate that applied to them. The *Time* chapter covers why funding counts seconds and freshness counts slots.

![](figures/perps-funding-index.svg)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Financial code fails at the edges the happy path never reaches: the refund, the
- **Test every attack by name.** Every rule in a program chapter's failure-modes section gets a test that tries to break it: the non-maker canceling an offer, the fee vault passed in place of the user vault, the second claim on a settled event.
- **Assert invariants, not just account values.** Conservation (a round trip creates no value), monotonicity (an index never decreases), directional rounding (no path rounds in the user's favor). An invariant test catches a whole bug class; an assertion on one account's value catches one instance of it.

Every test in this list is cheap to write and fast to run. The test runs the compiled program in-process in milliseconds, warping the slot makes weeks of interest accrue inside one test, and an injected feed account sets whatever price the test needs. Cashio and Wormhole, in the *Adversary* chapter, each lost their funds through a handler that accepted a substituted account. The fifth rule above turns that attack into a test: the fee vault passed in place of the user vault.
Every test in this list is cheap to write and fast to run. The test runs the compiled program in-process in milliseconds, moving the clock forward makes weeks of interest accrue inside one test, and an injected feed account sets whatever price the test needs. Cashio and Wormhole, in the *Adversary* chapter, each lost their funds through a handler that accepted a substituted account. The fifth rule above turns that attack into a test: the fee vault passed in place of the user vault.

## Proving Goes Further Than Testing

Expand Down
Loading
Loading