diff --git a/WRITING-PLAN.md b/WRITING-PLAN.md index 25af5185..61fd0384 100644 --- a/WRITING-PLAN.md +++ b/WRITING-PLAN.md @@ -7,7 +7,7 @@ How the manuscript gets produced, verified, reviewed, and kept alive. The table - The book prints the durable layer. Every code claim is verified against the companion repository before it lands: the skill's "grep before naming" rule, applied to a manuscript. - Diagrams are the longest-lived medium. Every chapter budget includes them: account map, state machine, flow. - Style is inherited wholesale from the [companion AI skill](https://github.com/quicknode/solana-finance-claude-plugin): onchain and offchain unhyphenated, "instruction handlers" for the functions and "instructions" for their input, "minor unit" not "base unit", one name per role enforced within each chapter, concrete two-asset examples, real handler names in lifecycle prose, persona-driven walkthroughs. Also from the skill, and deliberately not repeated below: handler naming (`initialize_x()` for setup, the domain's verb otherwise), PDA versus account, mint versus token, example assets, plain language and concrete definitions, emphasis, stating the mechanism rather than gesturing at it, and talking to readers rather than about the document. **A rule that would apply to any Solana finance writing belongs in the skill; this file adds only what is specific to the book.** `scripts/lint-manuscript.mjs` still enforces several of the skill's rules over the manuscript, so a rule can live there and be guarded here. -- Banned phrases, enforced by `scripts/lint-manuscript.mjs` (`npm run lint`) over the manuscript files: "the oracle problem", "instrument" (say "financial software" or "program"), the hyphenated chain words, and em-dashes. The lint also bans a group of phrases that read as machine-written, on the same grounds as the em-dash: a reader clocks them before finishing the sentence, and every one of them stands in for a mechanism rather than stating it. "Load-bearing" is the type specimen; say what depends on the thing and what breaks without it. The rest are "delve", "tapestry", "intricate", "multifaceted", "holistic", "myriad", "seamless", "robust", "pivotal", "crucial", "elevate", "testament to", "deep dive", "paradigm shift", "game-changer", "unlock the power", "realm of", "landscape of", "in today's", "it is worth noting", and "underscores the". The lint also rejects negative framing of offchain truth (any "cannot verify the price" phrasing): the manuscript teaches the positive mechanism, checking a fact's provenance, freshness, and confidence against a source the program trusts. "Smart contract" is banned in favour of "program", with exactly two exceptions, both contrastive: the Introduction's line telling a reader arriving from another chain what a Solana program is, and the glossary's definition. +- Banned phrases, enforced by `scripts/lint-manuscript.mjs` (`npm run lint`) over the manuscript files: "the oracle problem", "instrument" (say "financial software" or "program"), the hyphenated chain words, and em-dashes. The lint also bans a group of phrases that read as machine-written, on the same grounds as the em-dash: a reader clocks them before finishing the sentence, and every one of them stands in for a mechanism rather than stating it. "Load-bearing" is the type specimen; say what depends on the thing and what breaks without it. The rest are "delve", "tapestry", "intricate", "multifaceted", "holistic", "myriad", "seamless", "robust", "pivotal", "crucial", "elevate", "testament to", "deep dive", "paradigm shift", "game-changer", "unlock the power", "realm of", "landscape of", "in today's", "it is worth noting", and "underscores the". A second group is banned for announcing that a point exists instead of making it, each one written in this manuscript and each replaced by the content it stood in front of: "what matters", "the point is", "that is the whole reason", and "does the work of". "What matters here is the unit" became "the index advances by slots, not seconds"; delete the announcement and keep what followed it. The lint also rejects negative framing of offchain truth (any "cannot verify the price" phrasing): the manuscript teaches the positive mechanism, checking a fact's provenance, freshness, and confidence against a source the program trusts. "Smart contract" is banned in favour of "program", with exactly two exceptions, both contrastive: the Introduction's line telling a reader arriving from another chain what a Solana program is, and the glossary's definition. - An address is written as a markdown link, `[host.tld](https://host.tld)`, never as loose prose, because the PDF build only reaches links. It prints in the monospace face `program_id` uses, never underlined: underlining is a screen convention and collides with descenders in a serif face. A link whose text is words rather than an address gets that address printed after it in parentheses, since nobody can click a printed page. Both are done by `scripts/build-pdf.mjs`; the lint catches an address written as prose. - The publisher is **Quicknode**, one word, that capitalization. The legal entity is **Quiknode Inc.**, and it appears only where the entity is what is meant: the copyright holder, the liability disclaimer, the trademark owner, and the exclusive right to sell editions. There is no "Quicknode Press" imprint. `scripts/lint-manuscript.mjs` guards the spellings across the manuscript; `LICENSE.md` and `README.md` sit outside its globs and are checked by eye. - Numbers from outside the companion repository (fees, incident losses, market sizes) name their source in the text, link the primary publisher, and say where and when they apply. A figure that contradicts a reader's own experience is the normal case rather than the exception, because a cost that is typical in one country is unheard of in another: state the scope, so a reader whose transfers have all been domestic can check the claim in one click instead of concluding the book made it up. diff --git a/appendices/glossary.md b/appendices/glossary.md index 1f34da60..c973def6 100644 --- a/appendices/glossary.md +++ b/appendices/glossary.md @@ -58,6 +58,14 @@ An order to buy at or below a stated price. The buyer's side of an order book. A recent block's hash, included in every transaction to bound its lifetime. When it expires, the transaction can never land. +### 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. + +### built-in address + +An address chosen rather than computed: the System Program, the token programs, and the sysvars each sit at a fixed byte string the runtime treats as that thing. Neither a [public key](#address) somebody holds the private key for nor a [PDA](#pda-program-derived-address) derived from seeds, so a program uses it as a constant to compare against. Whether one happens to decode to a point on the ed25519 curve is an accident of the bytes and carries no meaning. + ### bump The extra byte found while deriving a PDA that pushes the result off the ed25519 curve. Programs store the bump at creation so later calls can rebuild the address cheaply. @@ -110,10 +118,6 @@ A permissionless instruction handler that performs maintenance work nobody owns A binary tree keyed on the first bit where entries differ. Used for order books because insertion never shifts other entries and walking it yields price-time priority for free. See the *Order Book Exchange* chapter. -### cumulative interest index - -A single monotonically growing number per reserve that starts at 1.0 and compounds the interest rate over time. Debts are stored scaled by the index at borrow time, so one multiplication accrues interest for every borrower at once. See the *Lending and Borrowing* chapter. - ### curator In a fund, the authority that decides which assets may be held at all and which price feed prices each one, kept separate from the [manager](#manager) who picks the allocation. The split is what stops a manager listing a token they minted themselves, or pairing a real asset with a feed they control. See the *Vault Strategy* chapter. @@ -520,7 +524,7 @@ A token account whose authority is a program's PDA, where a program holds funds ### WAD -A fixed-point convention: values scaled by 10^18, so 1.0 is stored as a 1 followed by eighteen zeros. The scale used for interest indexes and rate math. Borrowed from Ethereum, where DappHub's `ds-math` library introduced it and MakerDAO made it standard, at 18 decimals because that is what Ether and most ERC-20 tokens use. Now the usual word for 18-decimal fixed point on every chain, Solana included. +A fixed-point convention: values scaled by 10^18, so 1.0 is stored as a 1 followed by eighteen zeros. The scale used for accumulation factors and rate math. Borrowed from Ethereum, where DappHub's `ds-math` library introduced it and MakerDAO made it standard, at 18 decimals because that is what Ether and most ERC-20 tokens use. Now the usual word for 18-decimal fixed point on every chain, Solana included. ### wallet diff --git a/building-financial-products-in-solana/betting-market.md b/building-financial-products-in-solana/betting-market.md index df9aadf8..5c9e1628 100644 --- a/building-financial-products-in-solana/betting-market.md +++ b/building-financial-products-in-solana/betting-market.md @@ -47,7 +47,7 @@ After all three bets, the vault holds 600 USDC: 400 staked on Toy Story 5, 200 o ## The Film Grosses: The Operator Settles the Event -The month ends. In the real world, Toy Story 5 wins. Note what just happened in program terms: the fact that decides where 600 USDC goes exists entirely offchain. This program resolves it the simplest honest way there is: a designated trusted party signs. Maria calls `settle_event()` with the winning outcome's index, her signature is checked against the config's admin, and her word becomes the onchain truth. That is a **trust decision made visible**. The *Offchain Truth* chapter covers the full spectrum, from a single admin key like this one, through multi-party resolution, to signed price feeds; what matters is that the program makes the trusted party explicit and limits what their signature can do. Maria can pick which outcome won, but she cannot touch the pro-rata math, take more than the snapshotted fee, or reach into the vault by any other path. +The month ends. In the real world, Toy Story 5 wins. Note what just happened in program terms: the fact that decides where 600 USDC goes exists entirely offchain. This program resolves it the simplest honest way there is: a designated trusted party signs. Maria calls `settle_event()` with the winning outcome's index, her signature is checked against the config's admin, and her word becomes the onchain truth. That is a **trust decision made visible**. The *Offchain Truth* chapter covers the full spectrum, from a single admin key like this one, through multi-party resolution, to signed price feeds. This program makes the trusted party explicit and limits what their signature can do. Maria can pick which outcome won, but she cannot touch the pro-rata math, take more than the snapshotted fee, or reach into the vault by any other path. Settlement does four things, none of which involve paying bettors yet: diff --git a/building-financial-products-in-solana/figures/lending-account-map.svg b/building-financial-products-in-solana/figures/lending-account-map.svg index d128f1b1..c9b96582 100644 --- a/building-financial-products-in-solana/figures/lending-account-map.svg +++ b/building-financial-products-in-solana/figures/lending-account-map.svg @@ -264,8 +264,8 @@ USDC reserve available_liquidity: 1,250 - borrowed_amount_scaled: 750 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 750 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 @@ -310,8 +310,8 @@ NVDAx reserve available_liquidity: 5 - borrowed_amount_scaled: 0 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 0 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 @@ -356,8 +356,8 @@ SPCXx reserve available_liquidity: 1 - borrowed_amount_scaled: 3 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 3 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 diff --git a/building-financial-products-in-solana/figures/lending-borrow-ltv.svg b/building-financial-products-in-solana/figures/lending-borrow-ltv.svg index b0dc42bc..7671d6d4 100644 --- a/building-financial-products-in-solana/figures/lending-borrow-ltv.svg +++ b/building-financial-products-in-solana/figures/lending-borrow-ltv.svg @@ -203,8 +203,8 @@ USDC reserve available_liquidity: 250 - borrowed_amount_scaled: 750 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 750 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 @@ -246,8 +246,8 @@ NVDAx reserve available_liquidity: 5 - borrowed_amount_scaled: 0 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 0 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 diff --git a/building-financial-products-in-solana/figures/lending-deposit-shares.svg b/building-financial-products-in-solana/figures/lending-deposit-shares.svg index 8e536322..70d7c65f 100644 --- a/building-financial-products-in-solana/figures/lending-deposit-shares.svg +++ b/building-financial-products-in-solana/figures/lending-deposit-shares.svg @@ -149,8 +149,8 @@ USDC reserve available_liquidity: 1,000 - borrowed_amount_scaled: 0 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 0 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 diff --git a/building-financial-products-in-solana/figures/lending-liquidation.svg b/building-financial-products-in-solana/figures/lending-liquidation.svg index cbff658d..e067e54c 100644 --- a/building-financial-products-in-solana/figures/lending-liquidation.svg +++ b/building-financial-products-in-solana/figures/lending-liquidation.svg @@ -246,8 +246,8 @@ USDC reserve available_liquidity: 1,250 - borrowed_amount_scaled: 750 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 750 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 @@ -317,8 +317,8 @@ SPCXx reserve available_liquidity: 3.25 - borrowed_amount_scaled: 0.75 - cumulative_borrow_rate_index: 1.0 + borrowed_principal: 0.75 + borrow_accumulation_factor: 1.0 accumulated_protocol_fees: 0 diff --git a/building-financial-products-in-solana/figures/lending-rate-curve.svg b/building-financial-products-in-solana/figures/lending-rate-curve.svg index ab37c74c..84950d0a 100644 --- a/building-financial-products-in-solana/figures/lending-rate-curve.svg +++ b/building-financial-products-in-solana/figures/lending-rate-curve.svg @@ -27,8 +27,8 @@ past the kink the rate spikes, pushing borrowers to repay and pulling depositors in - - the index compounds at each refresh + + the factor compounds at each refresh @@ -44,6 +44,6 @@ refreshes, over five weeks of slots 1.0 ≈ 1.0107 - Bob’s debt = scaled amount × index: $750 → ≈ $758 + Bob’s debt = principal × factor: $750 → ≈ $758 one number reprices every loan in the reserve diff --git a/building-financial-products-in-solana/lending.md b/building-financial-products-in-solana/lending.md index c03c5f0d..d08f6ad5 100644 --- a/building-financial-products-in-solana/lending.md +++ b/building-financial-products-in-solana/lending.md @@ -10,7 +10,7 @@ One word needs defusing before anything else. This chapter's market trades stock The **lending market** PDA (seeds: `"lending_market"`, a market ID) stores the `owner` (Maria, used only for authorization, never in the address) and the `quote_currency_mint`, the stable asset all values are measured in: USDC. Distinct market IDs give independent, risk-isolated pools. -Each asset gets a **reserve** PDA (seeds: `"reserve"`, the market, the asset's mint). A reserve owns two accounts: a **liquidity vault** holding deposited tokens, and a **share mint** whose tokens are suppliers' claims on the vault plus its accrued interest. The reserve tracks `available_liquidity` (the un-borrowed pool, and deliberately the source of truth instead of the vault's raw balance, so donating tokens to the vault cannot move the exchange rate), `borrowed_amount_scaled`, the `cumulative_borrow_rate_index` that makes interest work (below), and `accumulated_protocol_fees`, Maria's slice, carved out so it never inflates the suppliers' exchange rate. +Each asset gets a **reserve** PDA (seeds: `"reserve"`, the market, the asset's mint). A reserve owns two accounts: a **liquidity vault** holding deposited tokens, and a **share mint** whose tokens are suppliers' claims on the vault plus its accrued interest. The reserve tracks `available_liquidity` (the un-borrowed pool, and deliberately the source of truth instead of the vault's raw balance, so donating tokens to the vault cannot move the exchange rate), `borrowed_principal`, the `borrow_accumulation_factor` that makes interest work (below), and `accumulated_protocol_fees`, Maria's slice, carved out so it never inflates the suppliers' exchange rate. Each reserve carries a `ReserveConfig`, all in basis points, and its validation rules are a compressed education in credit risk: @@ -33,7 +33,7 @@ Every valuation in this chapter multiplies an amount by a price, and the prices ## The Market Owner Opens the Market -Maria runs the market, and her revenue is a cut of the interest: the reserve factor you just met. She runs `initialize_lending_market()` with USDC as the quote currency, then `initialize_reserve()` three times, for USDC, NVDAx, and SPCXx, each with its config and price feed. Each reserve's `cumulative_borrow_rate_index` starts at exactly 1.0, represented as 10^18 in the program's fixed-point scale, chosen so a single slot's interest does not truncate to zero while staying far inside u128's ceiling. All the math in this program is integer-only: no floats, no fixed-point crates, just u128, a scale constant, and two rounding helpers. +Maria runs the market, and her revenue is a cut of the interest: the reserve factor you just met. She runs `initialize_lending_market()` with USDC as the quote currency, then `initialize_reserve()` three times, for USDC, NVDAx, and SPCXx, each with its config and price feed. Each reserve's `borrow_accumulation_factor` starts at exactly 1.0, represented as 10^18 in the program's fixed-point scale, chosen so a single slot's interest does not truncate to zero while staying far inside u128's ceiling. All the math in this program is integer-only: no floats, no fixed-point crates, just u128, a scale constant, and two rounding helpers. ## Supplying: Tokens In, Shares Out @@ -62,15 +62,15 @@ Read her position carefully, because everything later hangs on it: **Carol's deb ## Interest: One Index, No Transfers -Weeks pass, and the loans accrue interest without a single token moving. The mechanism is the **cumulative borrow-rate index**, and it is the most reusable idea in this chapter. +Weeks pass, and the loans accrue interest without a single token moving. The mechanism is the **borrow accumulation factor**, and it is the most reusable idea in this chapter. -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 index: new index = old index × (1 + rate per slot × elapsed slots). A loan is stored not as an amount but as a *scaled* amount: the amount divided by the index at borrow time. The live debt is always scaled amount × current index, rounded *up*, because debt rounds against the debtor. One multiplication per refresh serves every borrower in the reserve simultaneously; nobody's loan is ever visited individually. The moving of actual tokens is consolidated to the moments people repay or redeem, exactly 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 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. The moving of actual tokens is consolidated to the moments people repay or redeem, exactly 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%. The kink is the reserve begging for liquidity: when nearly everything is lent out, the rate spikes, which simultaneously pushes borrowers to repay and pulls new suppliers in with yield. It is monetary policy in fifteen lines of integer math. Of the interest that accrues, the reserve factor (10%) is skimmed into `accumulated_protocol_fees`, floored, in the suppliers' favor; the rest belongs to the suppliers through the rising share price. -![The kinked rate curve: borrow APR against utilization, gentle to the 80% kink and steep after it, alongside the cumulative index compounding across refreshes.](figures/lending-rate-curve.svg) +![The kinked rate curve: borrow APR against utilization, gentle to the 80% kink and steep after it, alongside the accumulation factor compounding across refreshes.](figures/lending-rate-curve.svg) ## The Shorted Asset Rallies and the Position Becomes Liquidatable @@ -101,7 +101,7 @@ Dave is one instance of a pattern this market runs on. Some work in a financial ## Repayment, Fees, and Yield: Everyone Leaves Whole -Five weeks in, Bob's cash crunch has passed. His $750 debt has quietly become about $758 through the index. He runs `repay_obligation_liquidity()` (anyone may repay any obligation, so there is no owner check; over-repayment clamps to the debt, and any sub-unit of principal lingers with the borrower rather than being forgiven by rounding). Then `withdraw_obligation_collateral()` releases his NVDAx shares (the withdraw math rounds *up* the borrow power being removed, so a withdrawal can never squeak past the health check by a rounding sub-unit), and `redeem_reserve_collateral()` turns shares back into his five NVDAx. Total cost of five weeks' liquidity: about eight dollars, and he never sold his NVIDIA. +Five weeks in, Bob's cash crunch has passed. His $750 debt has quietly become about $758 through the factor. He runs `repay_obligation_liquidity()` (anyone may repay any obligation, so there is no owner check; over-repayment clamps to the debt, and any sub-unit of principal lingers with the borrower rather than being forgiven by rounding). Then `withdraw_obligation_collateral()` releases his NVDAx shares (the withdraw math rounds *up* the borrow power being removed, so a withdrawal can never squeak past the health check by a rounding sub-unit), and `redeem_reserve_collateral()` turns shares back into his five NVDAx. Total cost of five weeks' liquidity: about eight dollars, and he never sold his NVIDIA. Maria runs `collect_protocol_fees()` whenever she likes and receives the accumulated 10% of interest, about 78 cents (`NothingToCollect` guards the empty case, and `protocol_fees_accrue_and_owner_can_collect` pins the flow). Of every dollar of interest Bob paid, ninety cents went to the suppliers, ten to the house. @@ -124,7 +124,7 @@ Every number in this program flows through `mul_div_floor()` or `mul_div_ceil()` - Total liquidity = available + borrowed-with-interest − the house's accumulated fees, and the share supply is a claim on exactly that; the exchange rate cannot be moved by donating tokens to the vault (pinned by `raw_token_donation_does_not_inflate_exchange_rate`). - No action may leave an obligation past its allowed borrow value, and every valuation is fresh (same transaction) or refused. - Loan-to-value ≤ liquidation threshold in every valid config, so there is always a buffer between the most you can borrow and the point you can be liquidated. -- The index never decreases, and debt computed from it never rounds in the borrower's favor. +- The factor never decreases, and debt computed from it never rounds in the borrower's favor. - Liquidation can only shrink an unhealthy position toward health, by at most the close factor, at exactly the bonus. ## The Failure Modes: The Liquidation Engine Is Only as Honest as Its Prices @@ -147,7 +147,7 @@ Strip the branding from a commercial bank and what remains is: pooled deposits e The concepts you now have: - **Shares as the yield mechanism.** Interest swells the pool while the share supply stays put, so each share redeems for more; no per-lender bookkeeping exists or needs to. -- **The cumulative index.** One multiplication accrues interest for every borrower simultaneously; debts are stored scaled and revalued on touch. The same shape prices any claim on a growing pool. +- **The accumulation factor.** Store each borrower's principal and one shared factor, and compute a debt by multiplying them. The same shape prices any claim on a growing pool. - **Conservative-by-construction valuation.** Collateral floors, debt ceils, so every health check errs against the borrower, and no rounding sub-unit can squeak a position past a boundary. - **Liquidation as a paid correction.** The close factor caps each call at part of the debt, so the borrower keeps what health does not require; the bonus pays the liquidator; the health gate means only positions already past the threshold can be touched. - **Cranks.** Permissionless, idempotent maintenance whose funds destinations come from stored state, and whose reliability comes from someone being paid. diff --git a/financial-programming-concepts-in-solana/the-arithmetic-of-money.md b/financial-programming-concepts-in-solana/the-arithmetic-of-money.md index 49c9f9c9..7554c799 100644 --- a/financial-programming-concepts-in-solana/the-arithmetic-of-money.md +++ b/financial-programming-concepts-in-solana/the-arithmetic-of-money.md @@ -75,9 +75,9 @@ Check them **after the math, not just before.** A check on the way in catches ba One warning from the same file: an invariant that can hold vacuously is not an invariant. `new_k >= old_k` passes trivially when `old_k` is zero, which is why the empty pool needs its own explicit guard. Know the degenerate values of every property you assert. -## One Index Compounds Interest for Every Borrower at Once +## One Accumulation Factor Compounds Interest for Every Borrower -Interest is multiplication repeated over time, and the naive implementation, looping over every borrower every slot, is impossible onchain. The standard solution is the **cumulative index**: one fixed-point number per market that starts at 1.0 and grows by the current rate each time anyone touches the market. New index = old index × (1 + rate per slot × elapsed slots). A debt is stored divided by the index at borrow time, and its live value is that scaled figure times the current index. One multiplication serves every borrower simultaneously, interest compounds across updates automatically, and no tokens move until someone actually repays or withdraws. The Lending chapter runs this machine in production; the same shape prices any share of a growing pool. +Interest is multiplication repeated over time, and the naive implementation, looping over every borrower every slot, is impossible onchain. The standard solution is the **accumulation factor**: one fixed-point number per market that starts at 1.0 and grows by the current rate each time anyone touches the market. New factor = old factor × (1 + rate per slot × elapsed slots). Instead of updating every debt, the market stores each borrower's principal and grows the one shared factor; a live debt is that principal multiplied by the current factor. Interest compounds across updates automatically, and no tokens move until someone actually repays or withdraws. The lending example calls the field `borrow_accumulation_factor`. The Lending chapter runs this machine in production; the same shape prices any share of a growing pool. Two numerical points complete the picture: @@ -92,4 +92,4 @@ Rates themselves are policy, not arithmetic, but one curve shows up everywhere: - Multiply before divide, promote intermediates to u128, and check every operation with a named overflow error. - Rounding is a policy decision: floor what the program pays, ceil what the user owes, round once, and keep health checks conservative. - Assert invariants after the math, and know the degenerate values where an invariant can hold vacuously. -- One cumulative index compounds interest for every borrower at once, and a utilization curve is monetary policy in fifteen lines of integer math. +- One accumulation factor compounds interest for every borrower at once, and a utilization curve is monetary policy in fifteen lines of integer math. diff --git a/prelude/introduction.md b/prelude/introduction.md index b38f2314..ffce8b65 100644 --- a/prelude/introduction.md +++ b/prelude/introduction.md @@ -71,7 +71,7 @@ The book has two parts. **The Fundamentals** is everything you need before writi **Building Financial Products** is nine chapters, and each one walks a complete financial program. A Solana program is code deployed onchain, and other blockchains call the same thing a smart contract. Between them the nine cover the financial products commonly built on Solana: escrow, crowdfunding, a betting market, an order book exchange, both kinds of automated market maker, lending and borrowing, a vault, and perpetual futures. Each chapter gives the accounts the program stores, the instruction handlers that move its money, the incentives of the people who use it and of whoever administers it, and the ways it can be attacked. Each is drawn as well as written: account maps, state machines, and value flows, in one visual language shared across all nine. These programs have not been drawn and explained side by side in one place before. -Real incidents appear throughout the book in boxed **War Story** sections: in the *Arithmetic of Money* chapter, the rounding bug that put about $2.6B at risk; in the *Adversary* chapter, the Mango Markets incident. History does not go stale, which makes these among the most durable pages in the book, and none of them are told to dunk on the teams involved - the point is the awareness. +Real incidents appear throughout the book in boxed **War Story** sections: in the *Arithmetic of Money* chapter, the rounding bug that put about $2.6B at risk; in the *Adversary* chapter, the Mango Markets incident. History does not go stale, which makes these among the most durable pages in the book, and none of them are told to dunk on the teams involved. They are here so you recognize the same shape in your own code before it costs you. To keep the money easy to track, every character starts every chapter with the same wallet: **one SOL** for rent deposits and transaction fees, **one thousand USDC**, and whatever story-specific assets the chapter hands them (a share grant, a stock position, a token inventory). When larger sums move, they belong to a firm's treasury or to a crowd, and the chapter says so. diff --git a/scripts/lint-manuscript.mjs b/scripts/lint-manuscript.mjs index 8bea4e8f..65de37df 100644 --- a/scripts/lint-manuscript.mjs +++ b/scripts/lint-manuscript.mjs @@ -38,6 +38,15 @@ const rules = [ [/\b(delve|tapestry|intricate|multifaceted|holistic|myriad|seamlessly|seamless|robust|pivotal|crucial|elevate|testament to|deep dive|paradigm shift|game.chang(er|ing)|unlock the power|realm of|landscape of|in today's)\b/i, "reads as machine-written; name the specific thing instead"], [/it(?:'s| is) worth noting/i, "cut it: if the point is worth making, make it"], + // Announcing that a point exists instead of making it. Each of these was + // written in this manuscript and each was replaced by the content it was + // standing in front of: "what matters here is the unit" became "the index + // advances by slots, not seconds". Delete the announcement, keep what + // followed it. + [/\bwhat matters\b/i, 'say the thing that matters instead of announcing that something does'], + [/\bthe point (?:here )?is\b/i, 'state the point rather than pointing at it'], + [/\bthat is the whole reason\b/i, 'give the reason without announcing it: "they count slots because ..."'], + [/\bdoes the work of\b/i, "describe what the code stores and computes, not how clever the result is"], [/\bunderscor(es|ing) (the|that|how|why|its)\b/i, 'say what it shows (the underscore character in identifiers is fine)'], [/can(?:not|'t) verify the price/i, "negative offchain-truth framing"], [/\bBONK\b/, "no meme tokens; use USDC and RWA stocks (NVDAx, TSLAx, SPCXx)"], diff --git a/solana-building-blocks/accounts-and-instruction-handlers.md b/solana-building-blocks/accounts-and-instruction-handlers.md index a749cd92..ee97bfe8 100644 --- a/solana-building-blocks/accounts-and-instruction-handlers.md +++ b/solana-building-blocks/accounts-and-instruction-handlers.md @@ -122,14 +122,15 @@ The programs themselves are stored the same way, and it takes more than one acco > **A deployed program is two accounts, and briefly three.** Under Solana's upgradeable loader, the account at the program's address is a stub of 36 bytes: it is marked executable, and its data is nothing but the address of a second account. That second account, **ProgramData**, holds the compiled code, the slot the program was last deployed in, and the **upgrade authority** permitted to replace it. A third account, the **buffer**, holds the new code while a deploy or an upgrade is being written, and is closed once the write lands so its rent comes back. Newer loaders fold this arrangement back into a single account. What stays true across all of them is that a program's code is not the thing sitting at the address you call. -## Addresses Come in Two Kinds +## Addresses Come in Three Kinds -Every account sits at a 32-byte **address**, usually shown base58-encoded. Addresses come in two kinds: +Every account sits at a 32-byte **address**, usually shown base58-encoded. Addresses come in three kinds: - **Public keys.** Alice's and Bob's wallet addresses are public keys: points on the curve, each with a private key that can sign for it. - **PDAs** (program derived addresses). A PDA is computed from a program's address (its **program ID**) and some chosen seeds, and has no private key at all, so no signature for it can ever exist. PDAs can still own accounts, and the program that derived one can act as it by presenting the seeds; the PDAs chapter explains how, and why Solana keeps the word "sign" for something that involves no signature. The escrow program you will build in this book stores each offer at a PDA derived from the maker's address and an offer ID. +- **Built-in addresses.** The System Program, the two token programs, and the sysvars sit at addresses that were *chosen* rather than computed: somebody picked a readable byte string, and the runtime treats that exact address as the thing it names. There is no private key and no derivation to redo, so the address is a constant your program compares against. `SysvarC1ock11111111111111111111111111111111` is the Clock the Time chapter reads. Some of these byte strings happen to decode to points on the curve and some do not, and it buys nobody anything either way: no private key is known for any of them. -![Two kinds of address: public keys sit on the curve, each with a private key that signs for it; PDAs sit off the curve, derived from a program ID and seeds, with no private key: the deriving program "signs" by presenting the seeds.](figures/address-types.svg) +![Three kinds of address: a public key sits on the curve with a private key behind it, a PDA hangs below it derived from a program ID and seeds, and a built-in address hangs below with neither, fixed by convention.](figures/address-types.svg) Because addresses are how everything is found, financial programs lean on *derivable* addresses: put the market's pair, the user's key, or the offer's ID into the derivation, and anyone can compute where the account must be, no lookup table required. @@ -139,7 +140,7 @@ A week later it is Bob's turn to pay. He owes Alice twenty-five dollars, and dol SOL is the network's **native token**, the asset it understands without any program's help. Every other token is defined by a program instead, and is formally an **SPL token**, after the Solana Program Library the original token program shipped in. Most people simply call all of them tokens, and so does this book. Each distinct token is identified by its **mint**, the account that is the asset's identity onchain, and the Tokens chapter covers mints properly. -Bob's SOL lives in his own account, but his USDC does not: a token balance lives in a **token account** of its own. The one holding a given wallet's balance of a given token sits at an address anyone can derive from the two, and that is what matters here. Bob does not need to ask Alice where her USDC lives. His wallet computes where her account must be, and sends the tokens there. The Tokens chapter covers token accounts properly. +Bob's SOL lives in his own account, but his USDC does not: a token balance lives in a **token account** of its own. The one holding a given wallet's balance of a given token sits at an address anyone can derive from the two. Bob does not need to ask Alice where her USDC lives. His wallet computes where her account must be, and sends the tokens there. The Tokens chapter covers token accounts properly. So Bob signs a transaction carrying one instruction: the **Token Program**'s `transfer()` handler, moving 25 USDC from his USDC account to Alice's. The instruction says 25,000,000, because USDC's minor unit is a millionth, the same integer discipline as lamports. Bob's signature is what authorizes the debit, exactly as Alice's authorized hers: the Token Program checks that the account being debited belongs to Bob, and that Bob signed. Alice's USDC balance rises from 975 to 1,000 with no signature from her. @@ -158,5 +159,5 @@ Two payments toured every building block the rest of the book uses: - A transaction bundles **instructions**, each addressed to a program's **instruction handler**, and they succeed or fail together: atomicity is a primitive financial software designs with. - Accounts store data as well as balances: bytes at an address, with a lamport balance and an owner program that users invoke to change them. A deployed program takes accounts of its own, and the state it manages lives in accounts separate again. - Space is paid for with a **rent** deposit proportional to the account's size, and it is a deposit rather than a fee: closing the account returns it. -- Addresses come in two kinds: **public keys**, which private keys sign for, and **PDAs**, which have no key: the deriving program "signs" by presenting the seeds. +- Addresses come in three kinds: **public keys**, which private keys sign for; **PDAs**, which have no key, where the deriving program "signs" by presenting the seeds; and **built-in addresses**, chosen rather than computed, with neither a key nor seeds. - Instructions supply the accounts as arguments; instruction handlers check them. diff --git a/solana-building-blocks/figures/address-types.svg b/solana-building-blocks/figures/address-types.svg index c0d7dda5..60116f51 100644 --- a/solana-building-blocks/figures/address-types.svg +++ b/solana-building-blocks/figures/address-types.svg @@ -13,8 +13,21 @@ Alice’s address a public key, on the curve: Alice holds the private key - “OFFER” + MAKER’S ADDRESS + ID - + THE ESCROW PROGRAM ID + SysvarC1ock111… + + + + + Clock sysvar + + slot + unix_timestamp + + a built-in address + chosen, not computed: + no private key and no seeds + “OFFER” + MAKER’S ADDRESS + ID + + THE ESCROW PROGRAM ID derives diff --git a/solana-building-blocks/figures/clock-decision-tree.svg b/solana-building-blocks/figures/clock-decision-tree.svg new file mode 100644 index 00000000..9c1a06d8 --- /dev/null +++ b/solana-building-blocks/figures/clock-decision-tree.svg @@ -0,0 +1,42 @@ + + + + + Do you only need things handled + in the order they arrived? + + yes + + a counter of your own + neither clock, nothing to bend + + + no + + + Are you measuring the age of a + number someone else published? + + yes + + whichever unit they stamped + if slots, add the LastRestartSlot check + + + no + + + Did you promise a person this + would happen at a real-world time? + + yes + + the wall clock + leave a margin wider than a few seconds + + + no + + the slot count + the default, and it pauses with the chain + diff --git a/solana-building-blocks/figures/day-boundary.svg b/solana-building-blocks/figures/day-boundary.svg new file mode 100644 index 00000000..0c33c838 --- /dev/null +++ b/solana-building-blocks/figures/day-boundary.svg @@ -0,0 +1,22 @@ + + + contribute() while elapsed days < 7 + + + refund() once elapsed days ≥ 7 + + + 6 days and 12 hours floors to 6, so contributions are still open + + + day 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + the first tick of day 7 + one instant: no gap, no overlap + diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 2b81c9c1..c9d5dc54 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -1,48 +1,58 @@ # Chapter 5: Time -Solana has two clocks. Validators can nudge one of them, so measuring the wrong thing with it lets whoever profits from the error shift what your program computes. +Solana has two clocks: a slot counter that nobody can bend, and a wall clock that validators report and can shift by a few seconds. Measure money with the slot counter. -| Clock | Source | Guarantee | Use it for | -| --- | --- | --- | --- | -| **slot** | The Clock sysvar's count of block opportunities, roughly 400ms apart | Only goes up, at a rate nobody can meaningfully bend | Anything an adversary would profit from bending: price staleness, per-slot interest | -| **unix timestamp** | The same Clock sysvar, validator-reported wall time | Right to within a few seconds | Humans and long horizons: day-counted deadlines, annual fees, display | +| Clock | Source | Guarantee | +| --- | --- | --- | +| **slot** | The Clock sysvar's count of block opportunities, roughly 400ms apart | Only goes up, at a rate nobody can meaningfully bend | +| **unix timestamp** | The same Clock sysvar, validator-reported wall time | Right to within a few seconds | ## Slots Are Guaranteed to Progress -The **slot** counts block opportunities, roughly 400ms apart, and its progression is guaranteed by the runtime: slots only go up, at a rate nobody can meaningfully bend. Programs read the current slot from the **Clock sysvar**, a system account the runtime keeps fresh. +The **slot** counts block opportunities, roughly 400ms apart. Slots only go up, at a rate nobody can meaningfully bend, and programs read the current one from the **Clock sysvar**, a system account the runtime keeps fresh. -Money that accrues does so per slot, in both programs of this book that accrue anything. The lending market carries one number per reserve, a **cumulative index** that grows each time anyone touches the market: +Two programs in this book accrue value over time, and both count it in slots: the lending market accrues interest on debt, and the perpetual futures venue accrues funding on open positions. The lending market carries one number per reserve, a **borrow accumulation factor** that grows each time anyone touches the market: ``` -new index = old index × (1 + rate per slot × elapsed slots) +new factor = old factor × (1 + rate per slot × elapsed slots) ``` -Elapsed slots is the current slot minus the slot of the last refresh, which the reserve stores. Every borrower's debt is their scaled principal times that index, so one multiplication per refresh does the work of looping over every borrower every slot. The *Arithmetic of Money* chapter builds this properly; what matters here is the unit. +Elapsed slots is the current slot minus the slot of the last refresh, which the reserve stores. Rather than recalculating every borrower's debt, the market stores each borrower's principal and updates the one shared factor. A borrower's current debt is their principal multiplied by that factor. The *Arithmetic of Money* chapter covers how to build that factor safely. The factor advances by slots, not seconds. -The perpetual futures venue does the same for funding, storing `cumulative_funding` alongside `last_funding_slot`. Both are indexes, both advance by elapsed slots, and both are money. That is the whole reason they count slots: a trader who could stretch a slot could stretch what they owe. +The perpetual futures venue does the same for funding, storing `cumulative_funding` alongside `last_funding_slot`. Both accumulate, both advance by elapsed slots, and both decide what somebody owes. They count slots because a trader who could stretch a slot could stretch what they owe. ## Timestamps Are Validator-Reported The **unix timestamp** comes from the same Clock sysvar, but it is wall-clock time reported by validators rather than a count the runtime guarantees. It is right to within a few seconds, and validators can nudge it. -Wall-clock time is still the right tool in places. The vault strategy reaches for it twice, for two different reasons: +A few seconds decide real money wherever a boundary sits on a timestamp. If an auction closes, an option expires, or a penalty starts on one, whoever moves the clock decides which side a transaction lands on. Leave a margin wide enough that seconds cannot change the answer, and count in slots anything where they could. + +The order book exchange uses neither clock for the thing traders care most about: who queued first. Each resting order takes the next number from a counter the market keeps, and matching compares those numbers. The order also records a `timestamp`, but this is only kept so offchain tooling can show an age. A number that decides who fills first is not a number to let anyone influence. + +Wall-clock time is still the right tool in places. The vault strategy uses it twice, for two different reasons: - **Its annual management fee accrues by timestamp**, because a few seconds of drift measured against a year is nothing. -- **It checks its price feed's age in seconds rather than slots**, because the feed publishes a wall-clock time, and the comparison has to be made in the units the number arrived in. +- **It checks its price feed's age in seconds rather than slots.** A **price feed** is an account that some publisher keeps updated with a price and the time they published it. This one publishes a wall-clock time, so the comparison has to be made in the unit the number arrived in. -Where a program's own deadline is for humans, timestamps are what humans mean. +When the deadline is one you promised a person, use timestamps. A contributor told they have a week to fund means seven days of wall-clock time, not a count of slots. ## Every Consumer Sets Its Own Freshness Window -A price is only usable if it is recent, and how recent is a judgement each program makes for itself. Three of this book's programs make it differently: +A program reading a price has two questions to answer: + +- Whether the source can be trusted. +- Whether the price is recent enough. + +The *Offchain Truth* chapter answers the first. This chapter answers the second, and how recent counts as recent enough is a judgement each program makes for itself. Four of this book's programs make it differently: | Program | Refuses a price older than | Measured in | | --- | --- | --- | | the lending market | 25 slots, about ten seconds | slots | | the prop AMM | 150 slots, about a minute | slots | +| the perpetual futures venue | 150 slots, about a minute | slots | | the vault strategy | 60 seconds | seconds | -The two that pick the window pick slots. The vault is the exception that shows the rule: its number is in seconds because the feed it reads publishes a wall-clock time, so seconds is the unit it was handed. The *Offchain Truth* chapter covers what else a program checks about a price before it uses one. +Three of the four count slots. The vault counts seconds because the feed it reads publishes a wall-clock time, so seconds is the unit it was handed. The *Offchain Truth* chapter covers what else a program checks about a price before it uses one. ## Integer Day Counts Have a Boundary, and Both Sides Must Use It @@ -52,37 +62,65 @@ A clock gives a program seconds, so counting days means dividing by 86,400. That elapsed days = (now − time_started) / 86,400 ``` -`contribute()` requires that count to be *less than* the duration. `refund()` requires it to be *at least* the duration. One expression, compared both ways, so there is no gap and no overlap between "you may still contribute" and "you may now refund": at the moment the count reaches the duration, contributions turn off and refunds turn on, by arithmetic rather than by a second stored flag anybody has to keep in step. +Both handlers compare that one count against the same duration: + +- `contribute()` requires it to be *less than* the duration. +- `refund()` requires it to be *at least* the duration. + +One expression, compared both ways, so there is no gap and no overlap between "you may still contribute" and "you may now refund". At the moment the count reaches the duration, contributions turn off and refunds turn on, by arithmetic rather than by a second stored flag anybody has to keep in step. -The trap is what "seven days" then means. With day-granularity math the window ends at the first tick of day seven, not at some intuitive end of the seventh day. That is a fine rule as long as it is the rule you meant and the one you told your users. +The trap is what "seven days" then means: the window ends at the first tick of day seven, not at the end of the seventh day. + +![A seven-day window counted in whole days: contributions run to the first tick of day 7 and refunds start there, one expression deciding both, so the two windows meet at an instant with no gap and no overlap.](figures/day-boundary.svg) + +Say which edge you mean when you write the terms down. A contributor reads "seven days to fund" as a window closing at the end of the seventh day, and this one closes at its start, a full day earlier than they think. Either edge is defensible; only one is what your users were told. ## Slots Stop When the Chain Stops, and the Wall Clock Does Not Both clocks arrive in the same sysvar, so it is easy to assume they always move together. They come apart when the chain stops. -Solana would rather stop than be wrong. When validators cannot agree which block is real, block production halts instead of continuing down two conflicting histories, and the network sits still until enough of the stake agrees where to resume. Mainnet has spent hours in that state. That is the trade financial software wants, because a chain that kept producing blocks through a disagreement could settle the same balance twice. +Solana would rather stop than be wrong. When validators cannot agree which block is real, block production halts instead of continuing down two conflicting histories, and the network sits still until enough of the stake agrees where to resume. That is the trade financial software wants, because a chain that kept producing blocks through a disagreement could settle the same balance twice. + +A halt is a rare event: at the time of writing, block production has not stopped for years. Write for the one you will probably never see. + +No block is produced during a halt, so the slot count does not move. Wall-clock time does. The first block after a restart carries a slot roughly where the last one left off and a timestamp hours later. Three consequences: -No block is produced during a halt, so the slot count does not move. Wall-clock time does. The first block after a restart therefore carries a slot roughly where the last one left off and a timestamp hours later: everything measured in slots was paused, and everything measured in seconds was running the whole time. Three consequences, one for each way this book's programs use a clock: +- **Per-slot accrual pauses with the chain.** The lending market's factor advances by elapsed slots, so a nine-hour halt accrues no interest: nobody is charged for a market they could not reach. A fee accruing by timestamp charges the full nine hours. +- **A window counted in seconds refuses the old price. One counted in slots accepts it, which is the dangerous half.** The vault strategy refuses a price older than sixty seconds, so its first call after a halt fails until a publisher posts again. The lending market's 25-slot window sees a price stamped one slot ago and, on the slot count alone, would accept a number nine hours old. A price stamped one slot ago is not always a price from four hundred milliseconds ago. +- **A deadline can pass with nobody able to act on it.** The fundraiser's day count runs through a halt like any other nine hours, so a contributor with an hour left finds the window shut when the chain returns. That program survives it because its refund path has no deadline; a design whose only exit was a narrow window would have stranded them. -- **Per-slot accrual pauses with the chain.** The lending market's index advances by elapsed slots, so a nine-hour halt accrues no interest at all: nobody is charged for a market they could not reach. A fee accruing by timestamp charges the full nine hours, because from its point of view nine hours is what happened. -- **A freshness window measured in seconds is blown at the restart, and one measured in slots is not.** The vault strategy refuses a price older than sixty seconds, so its first call after a halt fails until a publisher posts again. The lending market's 25-slot window sees a price stamped one slot ago, and by the slot count alone it would accept a number that is nine hours old. A price stamped one slot ago is not always a price from four hundred milliseconds ago. -- **A deadline can pass with nobody able to act on it.** The fundraiser's day count runs through a halt like any other nine hours, so a contributor with an hour left can find the window shut when the chain returns. That program survives it because its refund path has no deadline; a design whose only exit was a narrow window would have stranded them. +A slot-counted window needs one more check. The [`LastRestartSlot` sysvar](https://docs.anza.xyz/runtime/sysvars) carries the slot of the most recent cluster restart, or zero if there has never been one, and a price from before that restart is stale however few slots have passed since. The lending market, the prop AMM, and the perpetual futures venue all make this check in their oracle readers: any price stamped at or before the restart slot is rejected with `PricePredatesRestart`, and the market pauses valuation until the publisher posts again. Make it in any program whose freshness window is counted in slots. -The slot-counted window has a companion check that closes its gap. The [`LastRestartSlot` sysvar](https://docs.anza.xyz/runtime/sysvars) carries the slot of the most recent cluster restart, or zero if there has never been one, and a price from before the last restart is stale however few slots have passed since. The lending market, the prop AMM, and the perpetual futures venue all make this check in their oracle readers: any price stamped at or before the restart slot is rejected with `PricePredatesRestart`, and the market pauses valuation until the publisher posts again. Make it in any program whose freshness window is counted in slots. +## Which Clock to Use for Each Job -## Choose the Clock by Asking Who Benefits From the Error +Three questions, and a default if you answer no to all of them. -Anything an adversary would profit from bending is measured in slots. Timestamps are for humans and long horizons, where seconds of slack are noise: display, record-keeping, and rates accrued over months. The vault strategy's annual management fee accrues by timestamp, because seconds of drift against a year is nothing; the lending market's per-slot interest does not. +![The decision tree: each question sends a yes to its answer on the right and a no down to the next question, so order of arrival takes a counter of your own, the age of a published number takes whatever unit its publisher stamped, a real-world time you promised a person takes the wall clock, and three noes leave the slot count.](figures/clock-decision-tree.svg) + +The order matters at the third question. A fundraiser's deadline is worth money and a validator would happily move it, which argues for slots, but the deadline is also a promise made to a person in days, and days are wall-clock time. The promise wins, and the margin handles the adversary: leave enough room that a few seconds cannot change who gets paid. ![The two clocks: the slot counter steps evenly and only forward; the wall clock wobbles with its validators. Choose by asking who benefits from the error.](figures/two-clocks.svg) -Another concern: what does this number do when the chain stops? One clock pauses with it and the other does not, so the answer decides whether a halt is nine hours your program charges for, nine hours it forgives, or nine hours it never notices. +## What the Programs in This Book Chose, and How It Holds Up + +Every program in Part II that touches time is in this table, with the two questions worth asking of any answer: what happens if validators lean on the seconds, and what happens if the chain stops. + +| Program | What it times | Its answer | If the seconds are nudged | If the chain halts | +| --- | --- | --- | --- | --- | +| token fundraiser | its funding deadline | whole days from one stored timestamp | a few seconds cannot move a count that floors to whole days | the deadline can pass unattended; refunds have no deadline of their own, so nobody is stranded | +| lending market | interest accrual, and price freshness | slots for both | nothing to bend | accrual stops, and `LastRestartSlot` rejects a price from before the restart | +| perpetual futures | funding accrual, and price freshness | slots for both | nothing to bend | the same, and it matters more: leverage multiplies a bad price across the book | +| prop AMM | price freshness | slots | nothing to bend | the same restart check, and the market refuses to quote until a publisher posts | +| vault strategy | an annual fee, and price freshness | timestamps for both | seconds measured against a year are nothing | the fee charges through the halt, and the price check fails closed on its own | +| order book | who queued first | a sequence number, neither clock | matching never reads a clock, so there is nothing to lean on | queue positions come back exactly as they were | ## What You Learnt - Solana has two clocks: the **slot**, whose progression the runtime guarantees, and the **unix timestamp**, which validators report. -- Choose the clock by asking who benefits from the error: slots for anything an adversary could profit from bending, timestamps for humans and long horizons. -- Money that accrues accrues per slot, as a **cumulative index** advanced by elapsed slots, so one multiplication replaces a loop over every account. +- Three questions decide the clock, asked in order: order of arrival takes a counter of your own, the age of a published number takes whatever unit its publisher stamped, and a real-world time you promised a person takes the wall clock. Three noes leave the slot count. +- To accrue interest or funding without touching every account, store each account's principal and one shared **accumulation factor** that advances by elapsed slots. A current balance is the principal multiplied by the factor. - A price needs a freshness window, each consumer sets its own, and the unit follows the source: slots where the program chooses, seconds where the feed published seconds. -- A deadline counted in whole days floors, so both sides of it must be computed from the same expression, and the window ends at the first tick of the last day. -- Solana halts rather than run two conflicting histories, and during a halt slots pause while wall-clock time keeps running: per-slot accrual stops, second-measured freshness windows blow past their limit at the restart, slot-measured ones are not blown, and deadlines expire with nobody able to act on them. A slot-counted freshness window is paired with a `LastRestartSlot` check, rejecting any price stamped at or before the restart. +- A deadline counted in whole days floors, so both sides of it must be computed from the same expression, and the window ends at the first tick of the last day. Say which edge you meant when you write the terms down. +- Where a clock would decide something valuable and neither clock is safe enough, count instead: the order book settles who queued first with a sequence number, and keeps its timestamp for display only. +- Solana halts rather than run two conflicting histories. During a halt slots pause and wall-clock time keeps running, so per-slot accrual stops, deadlines expire with nobody able to act on them, and a price that is hours old still looks one slot old. +- Pair every slot-counted freshness window with a `LastRestartSlot` check, which rejects any price stamped at or before the last restart.