From f2ae1520cef53e3bd43ab18a265bbfc010421e8e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:54:19 +0000 Subject: [PATCH 01/23] Say how rare a halt is, and what the alternative networks do The section described halts without saying how often they happen, which leaves a reader to assume they are routine. Name the last one (February 6, 2024, about five hours) from Solana's own incident report, and set it beside the ten-hour Visa Europe outage in 2018 so the reader sees that a network stopping is not a blockchain peculiarity. Both figures are sourced in the text, per the numbers rule in WRITING-PLAN. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 2b81c9c1..10c35072 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -60,7 +60,9 @@ The trap is what "seven days" then means. With day-granularity math the window e 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. + +Mainnet has spent hours in that state, though not for a long time: the last full outage was on February 6, 2024, when block production stopped for about five hours ([Solana's incident report](https://solana.com/news/02-06-24-solana-mainnet-beta-outage-report)). The networks a bank already runs on stop too. A failed datacenter switch took Visa's European network down for around ten hours on June 1, 2018, and 5.2 million of the 51.2 million transactions attempted during it never went through ([Computer Weekly](https://www.computerweekly.com/news/252443325/Visa-reveals-rare-datacentre-switch-fault-as-root-cause-of-June-2018-outage)). Write for the halt 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 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: From d5c7accde09db19ad6d1703daa839b782b0c79f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 21:58:14 +0000 Subject: [PATCH 02/23] Teach built-in addresses as the third kind The chapter said addresses come in two kinds, public keys and PDAs, which leaves the System Program, the token programs, and the sysvars unaccounted for. They are neither: nobody holds a key for them and no seeds derive them. Somebody chose the byte string and the runtime treats that exact address as the thing it names. The figure gains a third column: the Clock sysvar hanging below the curve like the PDA, captioned with its address where the PDA carries its seed list, and with no derivation arrow, because the absence is the point. Whether one of these addresses happens to decode to a curve point is an accident of the bytes, so the book says that and does not tabulate which ones do. Also haloes the PDA's seed label, which the cluster curve was cutting through at its right end. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- appendices/glossary.md | 4 ++++ .../accounts-and-instruction-handlers.md | 9 +++++---- .../figures/address-types.svg | 17 +++++++++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/appendices/glossary.md b/appendices/glossary.md index 94da614a..6327f4a5 100644 --- a/appendices/glossary.md +++ b/appendices/glossary.md @@ -54,6 +54,10 @@ 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. +### 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 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. diff --git a/solana-building-blocks/accounts-and-instruction-handlers.md b/solana-building-blocks/accounts-and-instruction-handlers.md index a749cd92..9ac700f6 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 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. @@ -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..3da717a9 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 key and no seeds + “OFFER” + MAKER’S ADDRESS + ID + + THE ESCROW PROGRAM ID derives From 23a2bc99b03d1bbd18585b675427f3cca3923a0d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:08:48 +0000 Subject: [PATCH 03/23] Say only that a halt is rare The paragraph carried two dated incidents and four figures to make one point: this almost never happens. Say that instead. "At the time of writing" dates the claim without printing a number that goes stale. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 10c35072..a3658cb1 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -62,7 +62,7 @@ Both clocks arrive in the same sysvar, so it is easy to assume they always move 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. -Mainnet has spent hours in that state, though not for a long time: the last full outage was on February 6, 2024, when block production stopped for about five hours ([Solana's incident report](https://solana.com/news/02-06-24-solana-mainnet-beta-outage-report)). The networks a bank already runs on stop too. A failed datacenter switch took Visa's European network down for around ten hours on June 1, 2018, and 5.2 million of the 51.2 million transactions attempted during it never went through ([Computer Weekly](https://www.computerweekly.com/news/252443325/Visa-reveals-rare-datacentre-switch-fault-as-root-cause-of-June-2018-outage)). Write for the halt you will probably never see. +Mainnet has spent hours in that state, but 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 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: From f6f9ed8d8bb3d67ab7a50676b5d57177ad2346bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 21:02:52 +0000 Subject: [PATCH 04/23] Give the clock section answers instead of a principle The section stated the rule and left the reader to apply it. Lead with a table of the jobs a program actually has (accrual, price freshness, human deadlines, annual rates, display), what to measure each in, and what goes wrong with that choice. The principle keeps one line underneath, where it belongs once the answers are in hand. Drops the closing halt paragraph: the table's watch-for column now carries what a halt does to each choice. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index a3658cb1..e091cbd0 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -72,13 +72,19 @@ No block is produced during a halt, so the slot count does not move. Wall-clock 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. -## Choose the Clock by Asking Who Benefits From the Error +## Which Clock to Use for Each Job -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. +| What you are measuring | Use | Watch for | +| --- | --- | --- | +| interest, funding, anything that accrues and is worth money | **slots** | accrual stops during a halt, so nobody pays for hours they could not trade. That is usually what you want, but say so in your docs rather than finding out during an incident | +| how fresh a price is | **whichever unit the feed stamps**: slots if it publishes a slot, seconds if it publishes a time | a slot-counted window needs the `LastRestartSlot` check above, because slots stop standing in for elapsed time across a restart. A window counted in seconds already fails closed | +| a deadline people agreed to: a week to fund, a quarter to redeem | **timestamps** | validators can nudge them by a few seconds, so never let a few seconds decide who gets paid, and compute both sides of the boundary from one stored expression | +| a rate quoted per year | **timestamps** | it charges straight through a halt. Nine hours against a year is a rounding error; against a week it is not | +| anything a person reads: receipts, reporting, display | **timestamps** | nothing. This is what wall-clock time is for | -![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) +Behind the table is one question: who profits if this number is wrong? Measure in slots anything an adversary would bend, and in timestamps anything only a human would notice. The lending market accrues interest per slot for the first reason; the vault strategy's annual management fee accrues by timestamp for the second. -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. +![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) ## What You Learnt From f68c75c7566e5a0798b830a41624afd50ab961b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:47:52 +0000 Subject: [PATCH 05/23] Say the point instead of announcing that there is one "The Arithmetic of Money builds this properly; what matters here is the unit" tells the reader a point exists without making it. The point is that the index advances by slots rather than seconds, so say that. Same fix for "that is the whole reason they count slots", which announces a reason before giving it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index e091cbd0..954a7915 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -17,9 +17,9 @@ Money that accrues does so per slot, in both programs of this book that accrue a new index = old index × (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. 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 covers how to build that index safely. It 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 are indexes, both advance by elapsed slots, and both are money. They count slots because a trader who could stretch a slot could stretch what they owe. ## Timestamps Are Validator-Reported From 2a4ee84fb651221a315202a835129c510ce90b01 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:50:07 +0000 Subject: [PATCH 06/23] Name the two accruing programs, and say what clock drift costs "Money that accrues does so per slot, in both programs of this book that accrue anything" made the reader hunt for the programs and left "accrues" without an object. Name them and say what accrues: the lending market accrues interest on debt, the perpetual futures venue accrues funding on open positions. "Validators can nudge it" stated the capability and stopped. Say what it buys: where an auction closes or an option expires on a timestamp, whoever moves the clock decides which side of the boundary a transaction lands on. Then give the two defenses, a margin wider than the drift, or slots. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 954a7915..f0919f29 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -11,7 +11,7 @@ Solana has two clocks. Validators can nudge one of them, so measuring the wrong 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. -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 **cumulative index** that grows each time anyone touches the market: ``` new index = old index × (1 + rate per slot × elapsed slots) @@ -25,6 +25,8 @@ The perpetual futures venue does the same for funding, storing `cumulative_fundi 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. +Seconds are enough to matter wherever a boundary is worth money. If an auction closes, an option expires, or a penalty starts on a timestamp, whoever can move the clock a little decides which side of that boundary a transaction lands on, and the money follows. So leave a margin wide enough that a few seconds cannot change the answer, and count in slots anything where they could. + Wall-clock time is still the right tool in places. The vault strategy reaches for 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. From 709345f56557b9e8bc6809ee67423a4514623c62 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:16:30 +0000 Subject: [PATCH 07/23] Make the Time chapter answer the questions it raises Final pass for practicality. Three additions and one correction. A per-program table closes the chapter: every program in Part II that touches time, what it times, the mechanism it chose, and how that choice behaves under the two threats the chapter has been building toward, validators leaning on the seconds and the chain stopping. A figure for the day boundary, which the prose described but never showed: the contribute window running to the first tick of day 7, the refund window starting at that same tick, one expression deciding both. With it, a paragraph on which edge "seven days" names, since a contributor reads the phrase as the end of the seventh day and this window closes at its start. The order book gets its own answer recorded: where a clock would decide something valuable, count instead. Price-time priority reads a sequence number, and the order's timestamp is kept for display, which the example's own comment states. Correction: the freshness-window table said three programs and listed three. The perpetual futures venue has a 150-slot window too, verified against MAX_PRICE_STALENESS_SLOTS in the companion repository. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- .../figures/day-boundary.svg | 22 ++++++++++++++ solana-building-blocks/time.md | 29 +++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 solana-building-blocks/figures/day-boundary.svg 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 f0919f29..a3e84309 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -27,6 +27,8 @@ The **unix timestamp** comes from the same Clock sysvar, but it is wall-clock ti Seconds are enough to matter wherever a boundary is worth money. If an auction closes, an option expires, or a penalty starts on a timestamp, whoever can move the clock a little decides which side of that boundary a transaction lands on, and the money follows. So leave a margin wide enough that a few seconds cannot change the answer, and count in slots anything where they could. +There is a third answer, and the order book exchange uses it: where a clock would decide something valuable, count instead. Price-time priority has to know who queued first, so the venue stamps each resting order with a sequence number it allocates as the order rests, and matching reads that. It still records a `timestamp` on the order, and the code says what the field is for: not used by matching, 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 reaches for 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. @@ -36,15 +38,16 @@ Where a program's own deadline is for humans, timestamps are what humans mean. ## 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 price is only usable if it is recent, and how recent 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. +The three 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. ## Integer Day Counts Have a Boundary, and Both Sides Must Use It @@ -58,6 +61,10 @@ elapsed days = (now − time_started) / 86,400 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. +![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. "Seven days to fund" reads to a contributor as a window that closes 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 of them 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. @@ -88,11 +95,27 @@ Behind the table is one question: who profits if this number is wrong? Measure i ![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) +## 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, 25 slots for the price | nothing to bend | accrual stops, and `LastRestartSlot` rejects a price from before the restart | +| perpetual futures | funding accrual, and price freshness | slots for both, 150 slots for the price | nothing to bend | the same, and it matters more: leverage multiplies a bad price across the book | +| prop AMM | price freshness | 150 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, 60 seconds for the price | 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 | + +Two patterns run through the table. Where a number is worth money, it is counted in something nobody can lean on: slots, or a sequence number of the program's own. Where a number is for people, it is a timestamp with enough margin that seconds cannot change the answer. + ## 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. - 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. +- 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, 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. From f5195a0e1292639f9ec737191f05d33256361edb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:31:04 +0000 Subject: [PATCH 08/23] Match the new column's icon to the restyled figure The built-in address column was drawn before the icons were restyled, so its Clock sysvar box kept the old hand-drawn table glyph while the offer account beside it had the new one. --- solana-building-blocks/figures/address-types.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana-building-blocks/figures/address-types.svg b/solana-building-blocks/figures/address-types.svg index 3da717a9..076a7fec 100644 --- a/solana-building-blocks/figures/address-types.svg +++ b/solana-building-blocks/figures/address-types.svg @@ -17,7 +17,7 @@ - + Clock sysvar slot From 2b8ab929b6b4aff043c1315a82f9e8311aa2260d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:35:04 +0000 Subject: [PATCH 09/23] Say private key where the private key is what is missing A built-in address that decodes to a curve point has a perfectly well known public key: it is the address, printed on the page. What nobody holds is the private key. Three places said "key" where only the private half was meant, in the chapter, the glossary, and the figure caption. --- appendices/glossary.md | 2 +- solana-building-blocks/accounts-and-instruction-handlers.md | 2 +- solana-building-blocks/figures/address-types.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appendices/glossary.md b/appendices/glossary.md index 6327f4a5..4268fc86 100644 --- a/appendices/glossary.md +++ b/appendices/glossary.md @@ -56,7 +56,7 @@ A recent block's hash, included in every transaction to bound its lifetime. When ### 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 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. +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 diff --git a/solana-building-blocks/accounts-and-instruction-handlers.md b/solana-building-blocks/accounts-and-instruction-handlers.md index 9ac700f6..de073dfa 100644 --- a/solana-building-blocks/accounts-and-instruction-handlers.md +++ b/solana-building-blocks/accounts-and-instruction-handlers.md @@ -128,7 +128,7 @@ Every account sits at a 32-byte **address**, usually shown base58-encoded. Addre - **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 key is known for any of them. +- **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. ![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) diff --git a/solana-building-blocks/figures/address-types.svg b/solana-building-blocks/figures/address-types.svg index 076a7fec..60116f51 100644 --- a/solana-building-blocks/figures/address-types.svg +++ b/solana-building-blocks/figures/address-types.svg @@ -25,7 +25,7 @@ a built-in address chosen, not computed: - no key and no seeds + no private key and no seeds “OFFER” + MAKER’S ADDRESS + ID + THE ESCROW PROGRAM ID From edecfd5744dc558aba366caf85b19069124cf534 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:39:13 +0000 Subject: [PATCH 10/23] Define a price feed before the chapter leans on one The chapter used price feeds fourteen times across its prose and three tables, three chapters before Offchain Truth introduces oracles. The reader had met the word once, in a list in the Introduction. Two fixes, both in place. The first prose mention now says what a price feed is: an account some publisher keeps updated with a price and the time they published it. And the freshness section opens by splitting the two questions a program has about a price, sending trust to Offchain Truth and keeping recency here, so a reader knows what they are not being asked to understand yet. --- solana-building-blocks/time.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index a3e84309..998788e8 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -32,13 +32,18 @@ There is a third answer, and the order book exchange uses it: where a clock woul Wall-clock time is still the right tool in places. The vault strategy reaches for 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. ## 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. Four 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. + +Offchain Truth 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 | | --- | --- | --- | From 4817e5f38bf653674c3ade14aea408be7548a798 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:48:31 +0000 Subject: [PATCH 11/23] Describe the mechanism, not how clever it is Four sentences summarized a technique instead of stating it, leaving the reader to reconstruct what was compressed: - "one multiplication per refresh does the work of looping over every borrower every slot" is the effect. The design is that the market stores each borrower's principal and one shared index, and computes a debt by multiplying them. - "there is a third answer, and the order book uses it" made the reader hunt for the answer. The order book takes the next number from a counter, and matching compares those numbers. - "both are money" for two indexes: they decide what somebody owes. - "where a program's own deadline is for humans, timestamps are what humans mean" is a riddle. A contributor told they have a week means seven days of wall-clock time, not a count of slots. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 998788e8..24e09411 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -17,9 +17,9 @@ Two programs in this book accrue value over time, and both count it in slots: th new index = old index × (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 covers how to build that index safely. It advances by slots, not seconds. +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 index. A borrower's current debt is their principal multiplied by that index. The *Arithmetic of Money* chapter covers how to build that index safely. It 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. They count slots because 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 are indexes, 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 @@ -27,14 +27,14 @@ The **unix timestamp** comes from the same Clock sysvar, but it is wall-clock ti Seconds are enough to matter wherever a boundary is worth money. If an auction closes, an option expires, or a penalty starts on a timestamp, whoever can move the clock a little decides which side of that boundary a transaction lands on, and the money follows. So leave a margin wide enough that a few seconds cannot change the answer, and count in slots anything where they could. -There is a third answer, and the order book exchange uses it: where a clock would decide something valuable, count instead. Price-time priority has to know who queued first, so the venue stamps each resting order with a sequence number it allocates as the order rests, and matching reads that. It still records a `timestamp` on the order, and the code says what the field is for: not used by matching, kept so offchain tooling can show an age. A number that decides who fills first is not a number to let anyone influence. +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`, and the example's own comment says what that field is for: not used by matching, 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 reaches for 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.** 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 @@ -43,7 +43,7 @@ A program reading a price has two questions to answer: - Whether the source can be trusted. - Whether the price is recent enough. -Offchain Truth 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: +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 | | --- | --- | --- | From f97488547fd5713a56513ce59bdcb4153004492e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:52:56 +0000 Subject: [PATCH 12/23] Make a chapter reference look like one Cross-references named the chapter but never said it was a chapter, so "Offchain Truth answers the first" read as a sentence about offchain truth. Half the chapters are named after ordinary things (Time, Tokens, Escrow, Offchain Truth), so the reader had no way to tell a pointer from a subject. The form is now "the *Offchain Truth* chapter": italics delimit the title, the word says what it is, and a leading "The" is dropped inside the italics so "the *The Adversary* chapter" never happens. Applied to all 47 references across 16 files. WRITING-PLAN carries the convention, and lint-manuscript.mjs enforces it for the distinctive multi-word titles. The single-word ones are ordinary words too often to match mechanically, which the rule says. Also: "Mainnet has spent hours in that state" is "Mainnet has gone down". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 24e09411..510b6a3b 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -76,7 +76,7 @@ Both clocks arrive in the same sysvar, so it is easy to assume they always move 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. -Mainnet has spent hours in that state, but 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. +Mainnet has gone down, but 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 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: From 9ccb48d62f85753e0e91bb0ec774114f5dd0bb36 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:58:50 +0000 Subject: [PATCH 13/23] Give the clock rule a default instead of a symmetry "Measure in slots anything an adversary would bend, and in timestamps anything only a human would notice" presented two equal options and made the reader classify their case. Slots are the default; timestamps are the exception, and there are two of them. The old line also mislabeled one of those exceptions. An annual fee is not a number "only a human would notice", it is a number whose horizon is long enough that a few seconds of drift are noise. Both exceptions are now named for what they are. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 510b6a3b..f9978fcd 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -96,7 +96,7 @@ The slot-counted window has a companion check that closes its gap. The [`LastRes | a rate quoted per year | **timestamps** | it charges straight through a halt. Nine hours against a year is a rounding error; against a week it is not | | anything a person reads: receipts, reporting, display | **timestamps** | nothing. This is what wall-clock time is for | -Behind the table is one question: who profits if this number is wrong? Measure in slots anything an adversary would bend, and in timestamps anything only a human would notice. The lending market accrues interest per slot for the first reason; the vault strategy's annual management fee accrues by timestamp for the second. +Measure time in slots by default, because validators can nudge the wall clock and nobody can meaningfully bend the slot count. Reach for timestamps in two cases: when the number is one a person reads, like a deadline you promised or a date on a receipt, and when the horizon is long enough that a few seconds are noise, like a fee quoted per year. ![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) From 700c0123b7c9483bba2afe3b4f9f01120def0fd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 22:04:54 +0000 Subject: [PATCH 14/23] Say what the timestamp field is for, not who says so "The example's own comment says what that field is for" cites the source in front of the reader. The citation was evidence for the writer; the reader needs the fact. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index f9978fcd..eb0411c7 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -27,7 +27,7 @@ The **unix timestamp** comes from the same Clock sysvar, but it is wall-clock ti Seconds are enough to matter wherever a boundary is worth money. If an auction closes, an option expires, or a penalty starts on a timestamp, whoever can move the clock a little decides which side of that boundary a transaction lands on, and the money follows. So leave a margin wide enough that a few 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`, and the example's own comment says what that field is for: not used by matching, kept so offchain tooling can show an age. A number that decides who fills first is not a number to let anyone influence. +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 reaches for it twice, for two different reasons: From 3e68fcc51434a5db4fc15cec4db9e88c9f323370 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 21:56:52 +0000 Subject: [PATCH 15/23] Ban the phrases that announce a point instead of making it Four patterns, each written in this manuscript and each replaced by the content it was standing in front of: - "what matters here is the unit" became "the index advances by slots, not seconds" - "the point is the awareness" became a sentence saying what the War Stories are for - "that is the whole reason they count slots" became "they count slots because ..." - "one multiplication does the work of looping over every borrower" became a description of what the market stores and what it computes Three of the four had a live instance in the manuscript, now rewritten. The lint catches the next one, which is the part that matters more than the rules: these were all written after the rule against gesturing already existed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- WRITING-PLAN.md | 2 +- building-financial-products-in-solana/betting-market.md | 2 +- prelude/introduction.md | 2 +- scripts/lint-manuscript.mjs | 9 +++++++++ .../accounts-and-instruction-handlers.md | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) 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/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/prelude/introduction.md b/prelude/introduction.md index 8335983a..272b8735 100644 --- a/prelude/introduction.md +++ b/prelude/introduction.md @@ -65,7 +65,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 de073dfa..ee97bfe8 100644 --- a/solana-building-blocks/accounts-and-instruction-handlers.md +++ b/solana-building-blocks/accounts-and-instruction-handlers.md @@ -140,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. From 8c70729bc81d6411df5e76df4e2e0145b099ceb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 22:07:38 +0000 Subject: [PATCH 16/23] Audit the Time chapter for plain speaking One real clarity bug and eleven sentences that summarized instead of stating. The bug: "a freshness window measured in seconds is blown at the restart, and one measured in slots is not" taught the opposite of the lesson. Being blown is the safe outcome, so the bold lead made the correct behaviour sound like the failure and the dangerous behaviour sound fine. It now says which half is dangerous. The rest: - "measuring the wrong thing with it lets whoever profits from the error shift what your program computes" is "somebody else can move your numbers". - "the exception that shows the rule" is a cliche standing in front of a reason. The vault counts seconds because its feed publishes a wall-clock time. - "has a companion check that closes its gap" is "needs one more check". - "counted in something nobody can lean on" is "nobody can move". - "It advances by slots" could be the chapter or the index. Named. - "reaches for it twice" is "uses it twice". - The opening table still said slots were for what an adversary would bend, which contradicts the slots-by-default rule the chapter now gives. Both say default now. What You Learnt gets the same treatment: the clock bullet matches the default rule, the index bullet says what to store rather than what the multiplication saves, and the halt bullet was one sentence carrying four ideas and is now two bullets. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index eb0411c7..c492acec 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -1,15 +1,15 @@ # 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. Validators can nudge one of them, so if you measure money with that one, somebody else can move your numbers. | 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 | +| **slot** | The Clock sysvar's count of block opportunities, roughly 400ms apart | Only goes up, at a rate nobody can meaningfully bend | Everything by default, and money especially: price staleness, interest, funding | | **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 | ## 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. 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 **cumulative index** that grows each time anyone touches the market: @@ -17,7 +17,7 @@ Two programs in this book accrue value over time, and both count it in slots: th new index = old index × (1 + rate per slot × elapsed slots) ``` -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 index. A borrower's current debt is their principal multiplied by that index. The *Arithmetic of Money* chapter covers how to build that index safely. It advances by slots, not seconds. +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 index. A borrower's current debt is their principal multiplied by that index. The *Arithmetic of Money* chapter covers how to build that index safely. The index 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 decide what somebody owes. They count slots because a trader who could stretch a slot could stretch what they owe. @@ -29,7 +29,7 @@ Seconds are enough to matter wherever a boundary is worth money. If an auction c 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 reaches for it twice, for two different reasons: +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.** 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. @@ -52,7 +52,7 @@ The *Offchain Truth* chapter answers the first. This chapter answers the second, | the perpetual futures venue | 150 slots, about a minute | slots | | the vault strategy | 60 seconds | seconds | -The three 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 @@ -81,10 +81,10 @@ Mainnet has gone down, but a halt is a rare event: at the time of writing, block 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 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 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, which is what you want. The lending market's 25-slot window sees a price stamped one slot ago and, on the slot count alone, 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. -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. +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. ## Which Clock to Use for Each Job @@ -113,14 +113,15 @@ Every program in Part II that touches time is in this table, with the two questi | vault strategy | an annual fee, and price freshness | timestamps for both, 60 seconds for the price | 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 | -Two patterns run through the table. Where a number is worth money, it is counted in something nobody can lean on: slots, or a sequence number of the program's own. Where a number is for people, it is a timestamp with enough margin that seconds cannot change the answer. +Two patterns run through the table. Where a number is worth money, it is counted in something nobody can move: slots, or the program's own counter. Where a number is for people, it is a timestamp with enough margin that seconds cannot change the answer. ## 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. +- Use slots by default. Reach for timestamps in two cases: when a person reads the number, and when the horizon is long enough that a few seconds are noise. +- To accrue interest or funding without touching every account, store each account's principal and one shared **cumulative index** that advances by elapsed slots. A current balance is the principal multiplied by the index. - 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. 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, 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. +- 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. From 0eef24b513182d981e3d1c1ae38352c0ead20bc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 22:15:51 +0000 Subject: [PATCH 17/23] Cut the Time chapter back to what it needs The chapter had grown to 2,546 words, longer than The Adversary and nearly twice Tokens, for a subject that is two clocks. It is 2,336 now, and everything cut was said twice. - The opening table's "Use it for" column previewed the chapter's own conclusion, which two closing tables give properly. - The day-boundary edge had two paragraphs, added at different times, making the same point about "seven days" meaning day seven's first tick. - "Two patterns run through the table" restated the slots-by-default rule from fifteen lines earlier. - The roll-up repeated the four freshness numbers from the table above it. It carries the units now; the numbers stay where they are taught. - "Everything measured in slots was paused, and everything measured in seconds was running the whole time" restated the two sentences before it. - "Mainnet has gone down, but" in front of a sentence already saying halts are rare. What stayed: both closing tables, which answer different questions (which clock for a job, and what each program chose and how it holds up), the three halt consequences, which teach with numbers the tables have no room for, and the eight-bullet summary, one per section. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 42 ++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index c492acec..4ce1efcb 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -2,10 +2,10 @@ Solana has two clocks. Validators can nudge one of them, so if you measure money with that one, somebody else can move your numbers. -| 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 | Everything by default, and money especially: price staleness, interest, funding | -| **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 @@ -25,7 +25,7 @@ The perpetual futures venue does the same for funding, storing `cumulative_fundi 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. -Seconds are enough to matter wherever a boundary is worth money. If an auction closes, an option expires, or a penalty starts on a timestamp, whoever can move the clock a little decides which side of that boundary a transaction lands on, and the money follows. So leave a margin wide enough that a few seconds cannot change the answer, and count in slots anything where they could. +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. @@ -64,11 +64,11 @@ 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. -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. "Seven days to fund" reads to a contributor as a window that closes 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 of them is what your users were told. +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 @@ -76,13 +76,13 @@ Both clocks arrive in the same sysvar, so it is easy to assume they always move 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. -Mainnet has gone down, but 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. +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 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: +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: -- **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 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, which is what you want. The lending market's 25-slot window sees a price stamped one slot ago and, on the slot count alone, 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. +- **Per-slot accrual pauses with the chain.** The lending market's index 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. 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. @@ -90,10 +90,10 @@ A slot-counted window needs one more check. The [`LastRestartSlot` sysvar](https | What you are measuring | Use | Watch for | | --- | --- | --- | -| interest, funding, anything that accrues and is worth money | **slots** | accrual stops during a halt, so nobody pays for hours they could not trade. That is usually what you want, but say so in your docs rather than finding out during an incident | -| how fresh a price is | **whichever unit the feed stamps**: slots if it publishes a slot, seconds if it publishes a time | a slot-counted window needs the `LastRestartSlot` check above, because slots stop standing in for elapsed time across a restart. A window counted in seconds already fails closed | -| a deadline people agreed to: a week to fund, a quarter to redeem | **timestamps** | validators can nudge them by a few seconds, so never let a few seconds decide who gets paid, and compute both sides of the boundary from one stored expression | -| a rate quoted per year | **timestamps** | it charges straight through a halt. Nine hours against a year is a rounding error; against a week it is not | +| interest, funding, anything that accrues and is worth money | **slots** | accrual stops during a halt, so nobody pays for hours they could not trade. Say so in your docs | +| how fresh a price is | **whichever unit the feed stamps**: slots if it publishes a slot, seconds if it publishes a time | a slot-counted window needs the `LastRestartSlot` check above; a seconds-counted one already refuses stale prices on its own | +| a deadline people agreed to: a week to fund, a quarter to redeem | **timestamps** | validators can nudge them, so never let a few seconds decide who gets paid, and compute both sides of the boundary from one stored expression | +| a rate quoted per year | **timestamps** | it charges straight through a halt, which against a year is a rounding error and against a week is not | | anything a person reads: receipts, reporting, display | **timestamps** | nothing. This is what wall-clock time is for | Measure time in slots by default, because validators can nudge the wall clock and nobody can meaningfully bend the slot count. Reach for timestamps in two cases: when the number is one a person reads, like a deadline you promised or a date on a receipt, and when the horizon is long enough that a few seconds are noise, like a fee quoted per year. @@ -107,14 +107,12 @@ Every program in Part II that touches time is in this table, with the two questi | 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, 25 slots for the price | nothing to bend | accrual stops, and `LastRestartSlot` rejects a price from before the restart | -| perpetual futures | funding accrual, and price freshness | slots for both, 150 slots for the price | nothing to bend | the same, and it matters more: leverage multiplies a bad price across the book | -| prop AMM | price freshness | 150 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, 60 seconds for the price | seconds measured against a year are nothing | the fee charges through the halt, and the price check fails closed on its own | +| 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 | -Two patterns run through the table. Where a number is worth money, it is counted in something nobody can move: slots, or the program's own counter. Where a number is for people, it is a timestamp with enough margin that seconds cannot change the answer. - ## What You Learnt - Solana has two clocks: the **slot**, whose progression the runtime guarantees, and the **unix timestamp**, which validators report. From 4f65063ad27a83fdff01b7e334231e6cf9ee5307 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 14:25:07 +0000 Subject: [PATCH 18/23] Open the chapter with the answer, not a riddle "Somebody else can move your numbers" named neither who nor how much, and gave the reader nothing to do. Name both clocks, say which one validators can shift and by how much, and give the rule in the third sentence. --- solana-building-blocks/time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 4ce1efcb..4446b938 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -1,6 +1,6 @@ # Chapter 5: Time -Solana has two clocks. Validators can nudge one of them, so if you measure money with that one, somebody else can move your numbers. +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 | | --- | --- | --- | From 7b8db7f10196b101526373b0fef8c755975296b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 17:05:11 +0000 Subject: [PATCH 19/23] Replace the clock table with a decision tree The table listed jobs and left the reader to find theirs. A tree asks the questions instead, in an order that resolves the cases the table could not: four questions, and the first one you answer yes to is your answer. - only the order things arrived in? a counter of your own - the age of a number someone published? whichever unit they stamped - a real-world time you promised a person? the wall clock - anything else, and anything worth money the slot count The order carries a real decision. A fundraiser's deadline is worth money and a validator would happily move it, which argues for slots, but it is also a promise made to a person in days. The promise wins and the margin handles the adversary, so the promise question comes first. The prose under the figure says so, because a reader who reasons from the adversary alone will pick differently. Also gives the order book's counter and the "use the publisher's unit" rule a place in the answer, which the table never had. Chapter is 2,213 words, down from 2,336: the tree says more than the table did in less room. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- .../figures/clock-decision-tree.svg | 29 +++++++++++++++++++ solana-building-blocks/time.md | 14 ++++----- 2 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 solana-building-blocks/figures/clock-decision-tree.svg 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..78989017 --- /dev/null +++ b/solana-building-blocks/figures/clock-decision-tree.svg @@ -0,0 +1,29 @@ + + + What are you measuring? + + + only the order things arrived in? + + + a counter of your own + neither clock; nothing to bend + + the age of a number someone else published? + + + whichever unit they stamped + if slots, add the LastRestartSlot check + + a real-world time you promised a person? + + + the wall clock + leave a margin wider than a few seconds + + anything else, and anything worth money + + + the slot count + the default; pauses when the chain does + diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 4446b938..7b77eeea 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -88,15 +88,11 @@ A slot-counted window needs one more check. The [`LastRestartSlot` sysvar](https ## Which Clock to Use for Each Job -| What you are measuring | Use | Watch for | -| --- | --- | --- | -| interest, funding, anything that accrues and is worth money | **slots** | accrual stops during a halt, so nobody pays for hours they could not trade. Say so in your docs | -| how fresh a price is | **whichever unit the feed stamps**: slots if it publishes a slot, seconds if it publishes a time | a slot-counted window needs the `LastRestartSlot` check above; a seconds-counted one already refuses stale prices on its own | -| a deadline people agreed to: a week to fund, a quarter to redeem | **timestamps** | validators can nudge them, so never let a few seconds decide who gets paid, and compute both sides of the boundary from one stored expression | -| a rate quoted per year | **timestamps** | it charges straight through a halt, which against a year is a rounding error and against a week is not | -| anything a person reads: receipts, reporting, display | **timestamps** | nothing. This is what wall-clock time is for | +Four questions, asked in order. The first one you answer yes to is your answer. + +![A decision tree for measuring time: 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 everything else takes the slot count.](figures/clock-decision-tree.svg) -Measure time in slots by default, because validators can nudge the wall clock and nobody can meaningfully bend the slot count. Reach for timestamps in two cases: when the number is one a person reads, like a deadline you promised or a date on a receipt, and when the horizon is long enough that a few seconds are noise, like a fee quoted per year. +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) @@ -116,7 +112,7 @@ Every program in Part II that touches time is in this table, with the two questi ## What You Learnt - Solana has two clocks: the **slot**, whose progression the runtime guarantees, and the **unix timestamp**, which validators report. -- Use slots by default. Reach for timestamps in two cases: when a person reads the number, and when the horizon is long enough that a few seconds are noise. +- Four 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, a real-world time you promised a person takes the wall clock, and everything else takes the slot count. - To accrue interest or funding without touching every account, store each account's principal and one shared **cumulative index** that advances by elapsed slots. A current balance is the principal multiplied by the index. - 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. Say which edge you meant when you write the terms down. From a43e1d7a86c4f7d06520124815252e9ed7bf622a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 17:12:46 +0000 Subject: [PATCH 20/23] Draw the decision tree as a tree The first attempt was a spine with labels hanging off it, which is a flowchart's skeleton without its nodes. Each question is now a boxed node, and each edge is labelled: yes goes right to the answer, no goes down to the next question, and three noes fall through to the slot count. Square corners on every box, because rounded corners mean an account and nothing else. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- .../figures/clock-decision-tree.svg | 59 +++++++++++-------- solana-building-blocks/time.md | 4 +- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/solana-building-blocks/figures/clock-decision-tree.svg b/solana-building-blocks/figures/clock-decision-tree.svg index 78989017..9c1a06d8 100644 --- a/solana-building-blocks/figures/clock-decision-tree.svg +++ b/solana-building-blocks/figures/clock-decision-tree.svg @@ -1,29 +1,42 @@ - + - What are you measuring? - - only the order things arrived in? - - - a counter of your own - neither clock; nothing to bend + + Do you only need things handled + in the order they arrived? + + yes + + a counter of your own + neither clock, nothing to bend - the age of a number someone else published? - - - whichever unit they stamped - if slots, add the LastRestartSlot check + + no - a real-world time you promised a person? - - - the wall clock - leave a margin wider than a few seconds + + Are you measuring the age of a + number someone else published? + + yes + + whichever unit they stamped + if slots, add the LastRestartSlot check - anything else, and anything worth money - - - the slot count - the default; pauses when the chain does + + 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/time.md b/solana-building-blocks/time.md index 7b77eeea..2e8d3564 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -88,9 +88,9 @@ A slot-counted window needs one more check. The [`LastRestartSlot` sysvar](https ## Which Clock to Use for Each Job -Four questions, asked in order. The first one you answer yes to is your answer. +Three questions, and a default if you answer no to all of them. -![A decision tree for measuring time: 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 everything else takes the slot count.](figures/clock-decision-tree.svg) +![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. From db186fe261d559d220b03f5b00bc0d49dff3b177 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 17:27:27 +0000 Subject: [PATCH 21/23] Rename the lending index to the borrow accumulation factor The companion repository renamed `cumulative_borrow_rate_index` to `borrow_accumulation_factor` and `borrowed_amount_scaled` to `borrowed_principal`, because the old name described neither a rate nor what the number is for. The book followed, so no prose names a field that does not exist: - the Time chapter's formula block and the surrounding prose - the Arithmetic of Money heading and definition, which owns the concept - the lending chapter's account model and invariants - the glossary entry, retitled and moved into the B's - five lending figures that showed either identifier Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- appendices/glossary.md | 10 +++++----- .../figures/lending-account-map.svg | 12 ++++++------ .../figures/lending-borrow-ltv.svg | 8 ++++---- .../figures/lending-deposit-shares.svg | 4 ++-- .../figures/lending-liquidation.svg | 8 ++++---- .../figures/lending-rate-curve.svg | 6 +++--- building-financial-products-in-solana/lending.md | 12 ++++++------ .../the-arithmetic-of-money.md | 6 +++--- solana-building-blocks/time.md | 12 ++++++------ 9 files changed, 39 insertions(+), 39 deletions(-) diff --git a/appendices/glossary.md b/appendices/glossary.md index 4268fc86..90a64086 100644 --- a/appendices/glossary.md +++ b/appendices/glossary.md @@ -54,6 +54,10 @@ 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. @@ -106,10 +110,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. - ### decimals The number of minor units in one major unit of a token, declared on the mint. USDC has 6, SOL has 9. Programs must read decimals, never assume them. @@ -444,7 +444,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/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..c482a4ec 100644 --- a/building-financial-products-in-solana/lending.md +++ b/building-financial-products-in-solana/lending.md @@ -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/solana-building-blocks/time.md b/solana-building-blocks/time.md index 2e8d3564..c6fc28da 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -11,15 +11,15 @@ Solana has two clocks: a slot counter that nobody can bend, and a wall clock tha 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. -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 **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. Rather than recalculating every borrower's debt, the market stores each borrower's principal and updates the one shared index. A borrower's current debt is their principal multiplied by that index. The *Arithmetic of Money* chapter covers how to build that index safely. The index advances by slots, not seconds. +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 decide what somebody owes. They count slots because 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 @@ -80,7 +80,7 @@ A halt is a rare event: at the time of writing, block production has not stopped 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: -- **Per-slot accrual pauses with the chain.** The lending market's index 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. +- **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. @@ -113,7 +113,7 @@ Every program in Part II that touches time is in this table, with the two questi - Solana has two clocks: the **slot**, whose progression the runtime guarantees, and the **unix timestamp**, which validators report. - Four 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, a real-world time you promised a person takes the wall clock, and everything else takes the slot count. -- To accrue interest or funding without touching every account, store each account's principal and one shared **cumulative index** that advances by elapsed slots. A current balance is the principal multiplied by the index. +- 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. 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. From 3cb60d91b921419b4015f18d7808334a6019a0e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 17:27:58 +0000 Subject: [PATCH 22/23] Bullet the fundraiser's two deadline comparisons The pair of handler conditions is a two-item comparison a reader wants to hold side by side, so it reads better as a list than as a sentence. The prose after it keeps the reason the two conditions come from one expression. Also correct the What You Learnt summary, which said four questions decide the clock when the decision tree asks three and defaults on three noes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- solana-building-blocks/time.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index c6fc28da..c9d5dc54 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -62,7 +62,12 @@ 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: the window ends at the first tick of day seven, not at the end of the seventh day. @@ -112,7 +117,7 @@ Every program in Part II that touches time is in this table, with the two questi ## What You Learnt - Solana has two clocks: the **slot**, whose progression the runtime guarantees, and the **unix timestamp**, which validators report. -- Four 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, a real-world time you promised a person takes the wall clock, and everything else takes the slot count. +- 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. Say which edge you meant when you write the terms down. From 17e6b30da1c77162d4707afeec0e63c87f089f42 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 18:43:53 +0000 Subject: [PATCH 23/23] Rename the two lending.md occurrences the earlier pass missed The account-model paragraph and the reserve-setup paragraph still named `cumulative_borrow_rate_index` and `borrowed_amount_scaled`, neither of which exists in the companion repository any more. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj --- building-financial-products-in-solana/lending.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/building-financial-products-in-solana/lending.md b/building-financial-products-in-solana/lending.md index c482a4ec..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