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: 0 additions & 4 deletions appendices/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,6 @@ Rounding up to the next whole [minor unit](#minor-unit). The direction used for

Arithmetic that reports failure instead of producing a wrong answer: Rust's `checked_add`, `checked_sub`, `checked_mul` return nothing when the result will not fit, so the handler can fail with a named error. The alternative is [wrapping](#wrapping).

### checks-effects-interactions

The ordering rule for instruction handlers: validate everything, then update your own state, and only then transfer tokens or call other programs, so re-entry or hostile sequencing finds the program's state already updated. See the *Adversary* chapter.

### close factor

In a lending market, the fraction of a debt one liquidation may repay (typically 50%). See the *Lending and Borrowing* chapter.
Expand Down
5 changes: 1 addition & 4 deletions building-financial-products-in-solana/betting-market.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,7 @@ None of these payouts depended on when anyone bet. Had Alice bet first, when Toy

The multiply runs in u128 and divides exactly once, after the multiplication, so the result is floored a single time; dividing first would throw away precision on every claim. The floor means at most a few minor units of dust can remain in the vault, and dust belongs to the house, not to whichever claimant happened to arrive last.

Two orderings in this handler close attacks:

- **The bet is removed from the user's index before the token transfer.** Effects before interactions, so no re-entry path can see stale state.
- **The bet account itself closes at the end of the claim**, rent refunded to the bettor, and **a closed account is the double-claim defense**: Alice claiming twice fails because the bet account the second claim needs is already gone (checked by `test_double_claim_fails`).
The claim removes the bet from the user's index and closes the bet account, refunding its rent to the bettor. **A closed account stops a double claim**: Alice claiming twice fails because the bet account the second claim needs is already gone (checked by `test_double_claim_fails`).

There is no `claimed` boolean to forget to check.

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.
2 changes: 1 addition & 1 deletion building-financial-products-in-solana/fundraiser.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The test `test_stale_contribution_cannot_refund_from_next_raise` runs the attack

Now the other ending: the deadline passes with only 3,600 USDC in the counter, from Alice, Bob, Carol, and Dave, who contributed 800. No handler marks the raise as failed; `refund()` simply starts accepting calls. Alice calls it. The handler checks the deadline has passed (`FundraiserNotEnded` otherwise) and the target was missed (`TargetMet` otherwise). It then reads her receipt's `amount`, subtracts it from `current_amount`, zeroes the receipt, transfers exactly that amount from the vault back to her token account with the PDA's signature, and closes the receipt to return her rent.

The order inside that handler is checks-effects-interactions: the receipt is zeroed and the counter decremented *before* the token transfer. A second refund fails because the receipt it would read has been closed. The escrow closed its offer accounts for the same reason.
A second refund fails because the receipt it would read has been closed. The escrow closed its offer accounts for the same reason.

![](figures/fundraiser-refunds.svg)

Expand Down
2 changes: 1 addition & 1 deletion building-financial-products-in-solana/managed-fund.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Traditional funds sell assets to pay the management fee. This program does what

## Withdrawing In Kind

Alice takes profit with `withdraw()`, burning her 900 shares. The program does not sell anything to pay her: it transfers to her **her proportional slice of every vault, in kind**. That is her share of the recorded holdings in the USDC vault, the TSLAx vault (about 1.5), and the NVDAx vault (about 2.8), each amount floored, each destination account verified to be hers and of the right mint (`InvalidRecipient`). The share count is reduced before any token moves (checks-effects-interactions, as in every chapter), and her `min_usdc_out` floor protects the USDC leg (`test_withdraw_rejects_slippage`).
Alice takes profit with `withdraw()`, burning her 900 shares. The program does not sell anything to pay her: it transfers to her **her proportional slice of every vault, in kind**. That is her share of the recorded holdings in the USDC vault, the TSLAx vault (about 1.5), and the NVDAx vault (about 2.8), each amount floored, each destination account verified to be hers and of the right mint (`InvalidRecipient`). The share count drops by her 900 shares, and her `min_usdc_out` floor protects the USDC leg (`test_withdraw_rejects_slippage`).

She entered with $900, held through NVIDIA's rise, paid Maria 1% by dilution, and left with about $950 of assets. Bob still holds 450 shares, unrealized. Maria holds 13.5 shares, about $14, for managing the fund. Victor took no fee on any of it. What he gained is what his platform depends on: a track record of funds whose managers cannot take the assets.

Expand Down
2 changes: 1 addition & 1 deletion building-financial-products-in-solana/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ The handler checks the terms before it touches a token:
- **A zero premium** is refused the same way. It would make the option a gift rather than a sale.
- **An expiry at or before now** is refused (`ExpiryInPast`). The holder may exercise while `now < expiry`, so such an option could never be exercised.

Then the effects come before the transfer, in this order:
Then, before the transfer, the handler makes these changes in order:

1. The `OptionContract` is written with status `Listed` and an empty holder.
2. The market's `underlying_owed` rises by 5 NVDAx.
Expand Down
4 changes: 2 additions & 2 deletions building-financial-products-in-solana/order-book.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Traditional exchanges also let a trader shrink a resting order in place and keep

Unsettled balances are claims, not tokens. When Bob and Carol want their assets in hand, each calls `settle_funds()`, and the market PDA signs the transfers out of the vaults: Bob receives three NVDAx shares and his 250 USDC refund; Carol receives her 748.125 USDC.

One line in the handler belongs in everything you build: it snapshots the unsettled amounts and **zeroes the counters before making the transfers**. If a token transfer could ever call back into this program (token extensions can attach hooks to transfers), a caller re-entering mid-transfer would find balances already zeroed instead of still-credited, so double-withdrawal is impossible. Zeroing state before the transfer follows checks-effects-interactions, a rule the *Adversary* chapter explains fully.
The handler snapshots the unsettled amounts, **zeroes the counters**, and transfers the snapshot, so a user who calls `settle_funds()` again is paid nothing. If either transfer fails, the whole transaction is undone and the counters keep their values.

![](figures/order-book-settle.svg)

Expand Down Expand Up @@ -160,7 +160,7 @@ The order book added six concepts on top of the escrow's:

- **Every resting order is a funded commitment.** `place_order()` moves the tokens into a vault before the order rests, so no matched order can turn out to be backed by nothing.
- **A data structure chosen for onchain cost.** The critbit tree inserts without shifting other entries, and walking it visits orders in price-time priority, on a chain where compute is metered.
- **An internal ledger that consolidates payouts.** Unsettled balances turn many small payouts into one claimed transfer, and zeroing them before the transfer (checks-effects-interactions) makes double-withdrawal impossible.
- **An internal ledger that consolidates payouts.** Unsettled balances turn many small payouts into one claimed transfer, and zeroing them as they are paid out means each balance is paid once.
- **A fee whose rounding direction is a security decision.** Ceiling division on the fee closes a leak an attacker could exploit at scale through thousands of tiny fills.
- **Vault separation plus stored-address checks.** Two vaults holding the same token are indistinguishable to the token program; only stored-address checks keep users' balances and the market's fees apart.
- **A full book evicts its worst order.** Account space is finite, so a side that refused every order once full would let anyone with the capital to fill it keep every new order off it. Eviction replaces the order least likely to fill with a better one, refunding it as a cancel would.
Expand Down
2 changes: 1 addition & 1 deletion building-financial-products-in-solana/perpetual-futures.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,6 @@ This chapter added seven concepts:
- **Funding.** A pool-wide index plus a per-position entry snapshot charges the crowded side a fee without visiting every position. Production venues keep the same bookkeeping and scale the rate with the imbalance.
- **Mark-to-market share pricing.** LP shares price against AUM including traders' unrealized PnL, so entering and leaving the pool is fair at any instant.

This chapter used mints and token accounts, PDAs as vault authorities, shares priced against a pool, amounts read from state fields rather than vault balances, and checks-effects-interactions without explaining any of them, because earlier chapters already had. The same goes for a way out of every state for every participant, a signed external price checked before use, cranks run by profit-seekers, and integer arithmetic that rounds in the program's favor. In the *Where to Go From Here* chapter we will cover what this book left out, and how to take a program of your own from an idea to mainnet. The companion repository holds the code.
This chapter used mints and token accounts, PDAs as vault authorities, shares priced against a pool, and amounts read from state fields rather than vault balances without explaining any of them, because earlier chapters already had. The same goes for a way out of every state for every participant, a signed external price checked before use, cranks run by profit-seekers, and integer arithmetic that rounds in the program's favor. In the *Where to Go From Here* chapter we will cover what this book left out, and how to take a program of your own from an idea to mainnet. The companion repository holds the code.

[^perps-volume]: [defillama.com](https://defillama.com) tracks spot and perpetual volumes side by side, so the two can be compared on one page. It moves with the market.
8 changes: 3 additions & 5 deletions financial-programming-concepts-in-solana/the-adversary.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,13 @@ Fix this by following the rounding rules in the *Arithmetic of Money* chapter ev

A transaction can call your program, which calls another, which calls another. The flash loan in the *Core Building Blocks of Solana* chapter is built from programs calling programs. Each call is also a point where code you did not write runs: between your instructions in the same transaction, or inside a CPI your program makes.

Follow a rule called **checks-effects-interactions**: validate everything, then update your own state, and only then call other programs or move tokens. The order book's settlement zeroes a user's claimed balance *before* the transfer. If the token transfer could call back into the program (token extensions can attach hooks to transfers), the second call would find the balance already spent. Solana's runtime forbids a program from being re-entered through another program. Only a program calling itself directly is allowed, and its depth is capped. Keep the order anyway, because it also protects against changes you cannot see yet: a callee upgraded tomorrow, a hook added to a mint next year, an instruction placed after yours in the same transaction.

Three more rules for these boundaries:
Code inside a CPI cannot call back into your program. Solana's runtime refuses to invoke a program that is already on the call stack, unless the program is calling itself directly: if your program calls the token program, and a transfer hook on the mint tries to call your program, the transaction fails with `ReentrancyNotAllowed`. A callee upgraded tomorrow or a hook added to a mint next year meets the same refusal. And if any call fails, the whole transaction is undone, including every change your handler made before it. So the order in which a handler updates its own state and calls other programs does not open an attack. Three things at these boundaries do need care:

- **Validate what a CPI returns and what it was given.** Calling a program does not make the accounts it touched trustworthy. The managed fund computes each swap's minimum output from the oracle, because the router it calls is outside its control.
- **Read again what a CPI changed.** A framework that copies an account into memory when the handler starts keeps that copy through the CPI. After a transfer out of a vault, the copy still shows the old balance, and a check against it passes on a number that is no longer true. The AMM re-reads both vaults after its transfers, before it re-checks K.[^reload-per-framework]
- **Assume the instructions after yours are hostile.** Any invariant your program needs must hold at the end of *your* instruction, not at the end of the transaction, because the instructions after yours belong to someone else.

The *Arithmetic of Money* chapter's postcondition checks, such as re-checking K after the transfers land, follow the same rule: check your own state before anyone else's code runs.
The *Arithmetic of Money* chapter's postcondition checks, such as re-checking K after the transfers land, do this: they check the program's own state before anyone else's code runs.

## Every Role Holder Is Also a User

Expand All @@ -113,7 +111,7 @@ Where the role is the one reporting a result, like the betting market's admin se
- Build a defense against the inflation attack into any pool that issues shares: locked shares, virtual shares, a pool size kept in program state, or a large minimum first deposit.
- A true price can still be a manipulated price: time-weight it, act on the confidence interval, cap how much can move per window, and never lend more against a token than it would cost to move its market.
- A rounding error in the user's favor gets repeated until the pool is empty, so write a round-trip invariant test to catch it.
- Apply checks-effects-interactions at every boundary: your invariants must hold at the end of your own instruction, because the instructions after yours belong to someone else.
- Make every invariant hold at the end of your own instruction, because the instructions after yours belong to someone else.
- Treat everyone with a role as a user too: for each role, ask what its handlers can change, and what that change is worth to someone who also trades, deposits, or bets in the same program.

[^confused-deputy]: Norm Hardy named it in "The Confused Deputy (or why capabilities might have been invented)", ACM SIGOPS Operating Systems Review 22(4), October 1988, at [dl.acm.org/doi/10.1145/54289.871709](https://dl.acm.org/doi/10.1145/54289.871709).
Expand Down
Loading