From 113d81c25eb6d6333fe8a3877d9aa65bca4052a4 Mon Sep 17 00:00:00 2001 From: greaker <152405392+Greaker@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:05:50 +0200 Subject: [PATCH 1/3] API and UI: expose the government reserve; push WNOK balance changes live Replace the Central Bank resource's govSettlementBank field (a bank name resolved from BondManager.GOV_TBD) with govReserve: the reserve account address from BondManager.GOV_RESERVE and its live WNOK balance, which is what an operator checks before approving a payout. The lookup moves from the TBD roster module to the central-bank module; openapi.json is regenerated from the Zod schema. Coupon, redemption, and auction finalisation now decode nested reverts with the WNOK ABI instead of the TBD ABI, and publish bidders and central-bank live changes so dealer and reserve WNOK balances refresh after every cash movement (chain ingestion only marks bonds and auctions for those events). The treasury-held-units hint names the WNOK allowlist. The UI Central Bank page shows the reserve balance and address in place of the government bank KPI, and the pay-coupon warning points at the WNOK allowlist and the Central Bank page workaround. nb-bond-api: 245 jest tests green; nb-ui: 121 vitest tests green. Plan progress in docs/plans/bond-cash-leg-wnok/progress.md. --- docs/plans/bond-cash-leg-wnok/progress.md | 18 +++++----- services/nb-bond-api/openapi.json | 19 +++++----- services/nb-bond-api/src/app.ts | 36 +++++++++++-------- services/nb-bond-api/src/banking-tbd.ts | 18 +--------- services/nb-bond-api/src/central-bank.ts | 16 ++++++++- .../nb-bond-api/src/contracts/central-bank.ts | 13 ++++--- .../src/features/auctions/service.ts | 6 ++-- .../nb-bond-api/tests/custom-errors.test.ts | 27 +++++++++++--- services/nb-ui/src/pages/CentralBankPage.jsx | 14 +++++--- services/nb-ui/src/pages/PayCouponModal.jsx | 12 +++---- services/nb-ui/tests/CentralBankPage.test.jsx | 7 ++-- .../nb-ui/tests/CouponPayoutPage.test.jsx | 6 ++-- 12 files changed, 114 insertions(+), 78 deletions(-) diff --git a/docs/plans/bond-cash-leg-wnok/progress.md b/docs/plans/bond-cash-leg-wnok/progress.md index acf9713..36a328f 100644 --- a/docs/plans/bond-cash-leg-wnok/progress.md +++ b/docs/plans/bond-cash-leg-wnok/progress.md @@ -1,18 +1,18 @@ # Bond cash leg in wNOK — Progress **Plan:** [`plan.md`](plan.md) -**Last updated:** 2026-09-08 — Phase 1 done: `BondManager` settles every cash leg in WNOK from `GOV_RESERVE`; deploy scripts, tests, contract docs, and the API ABI artifact updated -**Current phase:** Phase 2: API contract and revert decoding -**Next action:** Open PR 1 from `feature/bond-cash-leg-wnok` (Phases 0 and 1); then Phase 2 step 1 — replace `getGovSettlementBank` with `getGovReserve` in `services/nb-bond-api/src/banking-tbd.ts` +**Last updated:** 2026-09-08 — Phases 2 and 3 done on `feature/bond-cash-leg-wnok-api` (stacked on PR #272): Central Bank resource exposes `govReserve`, payouts and finalisation push live changes, UI updated +**Current phase:** Phase 4: Fresh local sandbox validation (needs operator go-ahead to delete the local sandbox) +**Next action:** After PR #272 merges, rebase `feature/bond-cash-leg-wnok-api` onto `development`; run Phase 4 on a fresh sandbox; then Phase 5 docs and open PR 2 ## Phase Log | Phase | Status | Evidence | PR | |---|---|---|---| | 0 — Baseline and characterization | Done | baseline `forge test`: 24 suites, 390 passed; after adding assertions `BondManager.t.sol`: 47 passed (2026-09-07) | PR 1 | -| 1 — BondManager cutover, deploy wiring, contract docs, ADR | Done | `forge build`, `forge test`: 25 suites, 393 passed (3 new cases); verify-mapping check passed; `grep private-bank` on `BondManager.sol` empty; nb-bond-api lint, format, and 244 jest tests green with the refreshed ABI (2026-09-08) | PR 1 | -| 2 — API contract and revert decoding | Not started | | | -| 3 — UI alignment | Not started | | | +| 1 — BondManager cutover, deploy wiring, contract docs, ADR | Done (PR #272 open, all checks green) | `forge build`, `forge test`: 25 suites, 393 passed (3 new cases); verify-mapping check passed; `grep private-bank` on `BondManager.sol` empty; nb-bond-api lint, format, and 244 jest tests green with the refreshed ABI (2026-09-08) | PR 1 | +| 2 — API contract and revert decoding | Done | `govReserve {address, wnokBalance}` replaces `govSettlementBank`; `openapi.json` regenerated (one resource changed); coupon, redemption, and finalisation decode with the WNOK ABI and publish `bidders` + `central-bank`; nb-bond-api lint, format, build, 245 jest tests green (2026-09-08) | PR 2 | +| 3 — UI alignment | Done | Central Bank KPI shows the reserve's WNOK balance and address; coupon warning names the WNOK allowlist; nb-ui format, lint, build, 121 vitest tests green (2026-09-08) | PR 2 | | 4 — Fresh local sandbox validation | Not started | | | | 5 — Architecture docs, known issues, index | Not started | | | @@ -68,8 +68,10 @@ ## Session Handoff - Slither did not run locally (arm64); the Contracts CI job runs it on the PR. -- The API still calls `BondManager.GOV_TBD()` in `banking-tbd.ts`; with the refreshed ABI the - call rejects and the Central Bank route degrades the field to `null` until Phase 2 lands. +- `feature/bond-cash-leg-wnok-api` branches from `feature/bond-cash-leg-wnok` because PR #272 + was not yet merged; rebase onto `development` once it is. +- The new `getGovReserve` lives in `services/nb-bond-api/src/central-bank.ts` (a WNOK read), + not in the TBD roster module the old lookup came from. - The working tree on `development` carried unrelated uncommitted work when this plan was written: `ITbd` gained `mint`/`burn` declarations and an ERC-165 registration, and a new `contracts/src/settlement/` interbank-settlement set with its test. This plan does not touch diff --git a/services/nb-bond-api/openapi.json b/services/nb-bond-api/openapi.json index d1e2d55..7b13cf9 100644 --- a/services/nb-bond-api/openapi.json +++ b/services/nb-bond-api/openapi.json @@ -4169,22 +4169,23 @@ ], "description": "WNOK binding state. Null when WNOK is unreachable." }, - "govSettlementBank": { + "govReserve": { "anyOf": [ { "type": "object", "properties": { - "name": { - "type": "string", - "description": "Bank whose TBD settles government bond payments" - }, "address": { + "description": "BondManager.GOV_RESERVE — the government reserve account (a WNOK holder)", "$ref": "#/components/schemas/Address" + }, + "wnokBalance": { + "description": "Live WNOK balance of the reserve in 1-NOK units. Issuance credits it; buyback, coupon, and redemption debit it.", + "$ref": "#/components/schemas/BigIntString" } }, "required": [ - "name", - "address" + "address", + "wnokBalance" ], "additionalProperties": false }, @@ -4192,7 +4193,7 @@ "type": "null" } ], - "description": "BondManager.GOV_TBD resolved to a configured bank — the government's bond-payment settlement bank. Null when BondManager is unavailable." + "description": "The government reserve account every bond cash leg settles against, with its WNOK balance. Null when BondManager is unavailable." }, "md5": { "type": "string", @@ -4206,7 +4207,7 @@ "address", "available", "wnok", - "govSettlementBank", + "govReserve", "md5" ], "additionalProperties": false, diff --git a/services/nb-bond-api/src/app.ts b/services/nb-bond-api/src/app.ts index 277a28a..990ddac 100644 --- a/services/nb-bond-api/src/app.ts +++ b/services/nb-bond-api/src/app.ts @@ -107,12 +107,12 @@ import { } from './bidders'; import { BidderBidError, submitImpersonatedBid } from './bidder-bid'; import { - CentralBankNotConfiguredError, - WnokUnavailableError, addToAllowlist, burnWnok, + CentralBankNotConfiguredError, getCbAddress, getCbWnokBalance, + getGovReserve, getWnokTotalSupply, isCentralBankReady, listAllowlist, @@ -120,6 +120,7 @@ import { mintWnok, removeFromAllowlist, transferWnokFromCb, + WnokUnavailableError, } from './central-bank'; import { withMd5 } from './http'; import { provider } from './chain'; @@ -581,9 +582,9 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { // partition supply (CouponPaymentBalanceMismatch otherwise), so // treasury-held units — the unsold remainder the BondManager itself // keeps after a partial allocation — cannot be excluded here. When - // present they deadlock the payout on-chain: the government TBD's - // allowlist (correctly) refuses the manager contract, unless the - // operator explicitly allowlists it. See docs/KNOWN_ISSUES.md. + // present they deadlock the payout on-chain: the WNOK allowlist + // (correctly) refuses the manager contract, unless the operator + // explicitly allowlists it. See docs/KNOWN_ISSUES.md. const bondManager = await getBondManager(); const managerAddress = bondManager.target.toString().toLowerCase(); @@ -594,8 +595,11 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { opType: 'COUPON_PAYMENT', target: isin, detail: { holders: requested.length }, - interfaces: [bondManager.interface, new Interface(tbdAbi)], + interfaces: [bondManager.interface, new Interface(wnokAbi)], txHashOf: (sent) => sent.tx.hash, + // Coupon cash moves WNOK from the reserve to the holders: the + // Bidders and Central Bank pages show those balances live. + changedResources: ['bidders', 'central-bank'], }, () => sendWithManagedNonce(async (nonce) => { @@ -606,14 +610,14 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { } catch (err) { // Surface on-chain reverts readably; settlement failures wrap the // refusing token's own error in their lowLevelData bytes. - const description = describeRevert(err, [bondManager.interface, new Interface(tbdAbi)]); + const description = describeRevert(err, [bondManager.interface, new Interface(wnokAbi)]); if (description) { const treasuryHint = description.includes('AllowlistViolation') && description.toLowerCase().includes(managerAddress) ? ' — the BondManager holds unsold units from a partial allocation and is not ' + - 'allowlisted on the government settlement TBD; see docs/KNOWN_ISSUES.md for ' + - 'the workaround and the planned contract-side fix' + 'on the WNOK allowlist; allowlist it from the Central Bank page or see ' + + 'docs/KNOWN_ISSUES.md for the planned contract-side fix' : ''; throw conflict(`coupon payment reverted on-chain: ${description}${treasuryHint}`); } @@ -650,8 +654,10 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { opType: 'REDEMPTION', target: isin, detail: { holders: targetHolders.length }, - interfaces: [bondManager.interface, new Interface(tbdAbi)], + interfaces: [bondManager.interface, new Interface(wnokAbi)], txHashOf: (sent) => sent.tx.hash, + // Redemption cash moves WNOK from the reserve to the holders. + changedResources: ['bidders', 'central-bank'], }, () => sendWithManagedNonce(async (nonce) => { @@ -1006,7 +1012,7 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { : '0x0000000000000000000000000000000000000000', available: false, wnok: null, - govSettlementBank: null, + govReserve: null, }), ); return; @@ -1020,16 +1026,16 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { address: getCbAddress(), available: false, wnok: null, - govSettlementBank: null, + govReserve: null, }), ); return; } - const [balance, allowlist, totalSupply, govSettlementBank] = await Promise.all([ + const [balance, allowlist, totalSupply, govReserve] = await Promise.all([ getCbWnokBalance().catch(() => 0n), listAllowlist().catch(() => [] as string[]), getWnokTotalSupply().catch(() => 0n), - banking.getGovSettlementBank().catch(() => null), + getGovReserve().catch(() => null), ]); okResponse( req, @@ -1043,7 +1049,7 @@ export function createApp(dependencies: AppDependencies = {}): express.Express { totalSupply: totalSupply.toString(), allowlistSize: allowlist.length, }, - govSettlementBank, + govReserve, }), ); } catch (err) { diff --git a/services/nb-bond-api/src/banking-tbd.ts b/services/nb-bond-api/src/banking-tbd.ts index e3f6cba..e947a15 100644 --- a/services/nb-bond-api/src/banking-tbd.ts +++ b/services/nb-bond-api/src/banking-tbd.ts @@ -27,7 +27,7 @@ import { import { tbdAbi } from './abi'; import { deriveBidderAddress, fixtureRoleKey, fixtureRoleKeyOverride } from './bidders'; -import { getBondManager, getWnok, provider, resolveRegisteredAddress } from './chain'; +import { getWnok, provider, resolveRegisteredAddress } from './chain'; import { envVariables } from './env-vars'; import { withMd5 } from './http'; import { type IngestionDatabase, listBankRows } from './ingestion-db'; @@ -380,21 +380,6 @@ export async function listBanks( ); } -/** - * The bank whose tokenized deposit (TBD) settles government bond payments — - * `BondManager.GOV_TBD` resolved against the roster. Name is `'Unknown'` - * when GOV_TBD points at a TBD that is not in the roster. - */ -export async function getGovSettlementBank( - createdBanksDb: IngestionDatabase | null = null, -): Promise<{ name: string; address: string }> { - const manager = await getBondManager(); - const govTbd = getAddress((await manager.GOV_TBD()) as string); - const tbds = await resolveRosterTbds(createdBanksDb); - const match = tbds.find((t) => t.address === govTbd); - return { name: match?.bankName ?? 'Unknown', address: govTbd }; -} - /** Explicitly bound banking operations for one application/database instance. */ export function createBankingService(createdBanksDb: IngestionDatabase) { return { @@ -411,6 +396,5 @@ export function createBankingService(createdBanksDb: IngestionDatabase) { transferTbd: (address: string, to: string, amount: bigint) => transferTbd(address, to, amount, createdBanksDb), listBanks: () => listBanks(createdBanksDb), - getGovSettlementBank: () => getGovSettlementBank(createdBanksDb), }; } diff --git a/services/nb-bond-api/src/central-bank.ts b/services/nb-bond-api/src/central-bank.ts index 2db4593..e2ab988 100644 --- a/services/nb-bond-api/src/central-bank.ts +++ b/services/nb-bond-api/src/central-bank.ts @@ -23,7 +23,7 @@ import { } from 'ethers'; import { wnokAbi } from './abi'; -import { getWnokAddress, provider } from './chain'; +import { getBondManager, getWnokAddress, provider } from './chain'; import { envVariables } from './env-vars'; export class CentralBankNotConfiguredError extends Error { @@ -102,6 +102,20 @@ export async function getCbWnokBalance(): Promise { return bal; } +/** + * The government reserve account that every bond cash leg settles against: + * `BondManager.GOV_RESERVE`, with its live WNOK balance. Issuance credits it; + * buyback, coupon, and redemption debit it, so the balance is what an + * operator checks before approving a payout. + */ +export async function getGovReserve(): Promise<{ address: string; wnokBalance: string }> { + const manager = await getBondManager(); + const address = getAddress((await manager.GOV_RESERVE()) as string); + const wnok = await getWnokContract(); + const balance = (await wnok.balanceOf(address)) as bigint; + return { address, wnokBalance: balance.toString() }; +} + /** Read total WNOK supply (minted minus burned) via the bound contract. */ export async function getWnokTotalSupply(): Promise { const wnok = await getWnokContract(); diff --git a/services/nb-bond-api/src/contracts/central-bank.ts b/services/nb-bond-api/src/contracts/central-bank.ts index acdfe0f..6e99765 100644 --- a/services/nb-bond-api/src/contracts/central-bank.ts +++ b/services/nb-bond-api/src/contracts/central-bank.ts @@ -53,15 +53,20 @@ export const centralBankSchema = z .meta({ description: 'WNOK binding state. Null when WNOK is unreachable.', }), - govSettlementBank: z + govReserve: z .object({ - name: z.string().meta({ description: 'Bank whose TBD settles government bond payments' }), - address: addressSchema, + address: addressSchema.meta({ + description: 'BondManager.GOV_RESERVE — the government reserve account (a WNOK holder)', + }), + wnokBalance: bigIntStringSchema.meta({ + description: + 'Live WNOK balance of the reserve in 1-NOK units. Issuance credits it; buyback, coupon, and redemption debit it.', + }), }) .nullable() .meta({ description: - "BondManager.GOV_TBD resolved to a configured bank — the government's bond-payment settlement bank. Null when BondManager is unavailable.", + 'The government reserve account every bond cash leg settles against, with its WNOK balance. Null when BondManager is unavailable.', }), md5: md5Schema, }) diff --git a/services/nb-bond-api/src/features/auctions/service.ts b/services/nb-bond-api/src/features/auctions/service.ts index e4e3331..59486a8 100644 --- a/services/nb-bond-api/src/features/auctions/service.ts +++ b/services/nb-bond-api/src/features/auctions/service.ts @@ -1,6 +1,6 @@ import { Interface, keccak256, toUtf8Bytes } from 'ethers'; -import { tbdAbi } from '../../abi'; +import { wnokAbi } from '../../abi'; import { computeBuybackAllocation, computeUniformAllocation } from '../../allocation'; import { DependencyUnavailableError, type MutationResource } from '../../application-errors'; import { normalizeSealedBid, unsealBid } from '../../bid'; @@ -322,8 +322,10 @@ export function createAuctionService(dependencies: AuctionServiceDependencies) { opType: 'AUCTION_FINALISE', target: auctionId, detail: { isin, allocations: allocations.length }, - interfaces: [bondManager.interface, new Interface(tbdAbi)], + interfaces: [bondManager.interface, new Interface(wnokAbi)], txHashOf: (sent) => sent.tx.hash, + // Issuance and buyback cash move WNOK between bidders and the reserve. + changedResources: ['bidders', 'central-bank'], }, () => sendWithManagedNonce(async (nonce) => { diff --git a/services/nb-bond-api/tests/custom-errors.test.ts b/services/nb-bond-api/tests/custom-errors.test.ts index ab29d51..67c4440 100644 --- a/services/nb-bond-api/tests/custom-errors.test.ts +++ b/services/nb-bond-api/tests/custom-errors.test.ts @@ -1,5 +1,5 @@ import { Interface } from 'ethers'; -import { bondAuctionAbi, bondManagerAbi, tbdAbi } from '../src/abi'; +import { bondAuctionAbi, bondManagerAbi, tbdAbi, wnokAbi } from '../src/abi'; import { decodeCustomError, describeRevert } from '../src/chain'; describe('decodeCustomError', () => { @@ -41,10 +41,12 @@ describe('describeRevert', () => { const managerIface = new Interface(bondManagerAbi); const tbdIface = new Interface(tbdAbi); - // Real revert captured from a live coupon payment whose default holder - // list still included the BondManager itself (treasury-held units after - // a partial allocation): BondDvP wrapped the government TBD's - // AllowlistViolation in SettlementFailure's lowLevelData bytes. + // Real revert captured from a live coupon payment (when payouts still ran + // through a government TBD) whose default holder list included the + // BondManager itself (treasury-held units after a partial allocation): + // BondDvP wrapped the token's AllowlistViolation in SettlementFailure's + // lowLevelData bytes. The shape is identical for WNOK, which now settles + // every bond cash leg and shares the same Errors library. const settlementRevertData = '0xc974ce95' + '0000000000000000000000000000000000000000000000000000000000000001' + @@ -67,6 +69,21 @@ describe('describeRevert', () => { expect(described).toContain('0xe61a63Ef630b7B6FF9b9e595B61f641171a4eB97'); }); + it('decodes a WNOK-nested SettlementFailure with the WNOK interface', () => { + const wnokIface = new Interface(wnokAbi); + const inner = wnokIface.encodeErrorResult('AllowlistViolation', [ + 'Wholesale NOK', + '0xe61a63Ef630b7B6FF9b9e595B61f641171a4eB97', + 'recipient not on allowlist', + ]); + const wrapped = managerIface.encodeErrorResult('SettlementFailure', [1, inner]); + const described = describeRevert({ data: wrapped }, [managerIface, wnokIface]); + expect(described).toContain('SettlementFailure'); + expect(described).toContain('AllowlistViolation'); + expect(described).toContain('Wholesale NOK'); + expect(described).toContain('recipient not on allowlist'); + }); + it('describes a flat error without nesting', () => { const flat = tbdIface.encodeErrorResult('AllowlistViolation', [ 'TBD Nordea', diff --git a/services/nb-ui/src/pages/CentralBankPage.jsx b/services/nb-ui/src/pages/CentralBankPage.jsx index d50f70c..3d90fe6 100644 --- a/services/nb-ui/src/pages/CentralBankPage.jsx +++ b/services/nb-ui/src/pages/CentralBankPage.jsx @@ -2,7 +2,7 @@ * CentralBankPage — Norges Bank operator surface against WNOK. * * Surfaces: - * - KPIs: WNOK balance (at CB), supply, in-circulation, government settlement bank + * - KPIs: WNOK balance (at CB), supply, in-circulation, government reserve (WNOK balance) * - Addresses (CB account + WNOK contract) and the allowlist editor (add/remove) * - Mint / Burn / Transfer actions in the header (all transactions originate * from the CB account, gated by MINTER/BURNER roles + the WNOK allowlist) @@ -181,9 +181,15 @@ export function CentralBankPage() {
WNOK held outside the CB
-
Government bank
-
{cb.govSettlementBank?.name ?? '—'}
-
Settles bond coupon / redemption
+
Government reserve
+
+ {cb.govReserve ? Fmt.formatUnits(cb.govReserve.wnokBalance) : '—'} +
+
+ {cb.govReserve + ? `${Fmt.shortHex(cb.govReserve.address)} · pays coupon, buyback, redemption` + : 'BondManager unavailable'} +
diff --git a/services/nb-ui/src/pages/PayCouponModal.jsx b/services/nb-ui/src/pages/PayCouponModal.jsx index ad9b86a..d9ba1da 100644 --- a/services/nb-ui/src/pages/PayCouponModal.jsx +++ b/services/nb-ui/src/pages/PayCouponModal.jsx @@ -27,9 +27,9 @@ export function PayCouponModal({ bond, onClose, onPaid }) { // The on-chain payCoupon requires covering EVERY holder — including the // BondManager itself when a partial allocation left it holding unsold - // units. Such treasury-held units deadlock the payout (the government - // TBD's allowlist refuses the manager contract), so flag them loudly - // before the operator fires a doomed transaction. + // units. Such treasury-held units deadlock the payout (the WNOK + // allowlist refuses the manager contract), so flag them loudly before + // the operator fires a doomed transaction. const managerAddress = (bond.contracts?.manager ?? '').toLowerCase(); const holders = bond.holders ?? []; const isTreasury = (h) => (h.holder ?? '').toLowerCase() === managerAddress; @@ -111,9 +111,9 @@ export function PayCouponModal({ bond, onClose, onPaid }) { {treasuryHeld && (
This bond has treasury-held units — the unsold remainder sits on the bond manager after a - partial allocation. On-chain payout must cover every holder, and the government TBD's - allowlist blocks the manager, so this payment will fail unless the manager is explicitly - allowlisted there. See docs/KNOWN_ISSUES.md. + partial allocation. On-chain payout must cover every holder, and the WNOK allowlist blocks + the manager, so this payment will fail unless the manager is allowlisted from the Central + Bank page. See docs/KNOWN_ISSUES.md.
)} diff --git a/services/nb-ui/tests/CentralBankPage.test.jsx b/services/nb-ui/tests/CentralBankPage.test.jsx index 10652d3..49777af 100644 --- a/services/nb-ui/tests/CentralBankPage.test.jsx +++ b/services/nb-ui/tests/CentralBankPage.test.jsx @@ -16,7 +16,7 @@ const FIXTURE_CB = { totalSupply: '25000000', allowlistSize: 3, }, - govSettlementBank: { name: 'DNB', address: '0x3333333333333333333333333333333333333333' }, + govReserve: { address: '0x3333333333333333333333333333333333333333', wnokBalance: '7500000' }, md5: 'a', }; @@ -71,8 +71,9 @@ describe('CentralBankPage', () => { }); expect(screen.getByText('WNOK supply')).toBeInTheDocument(); expect(screen.getByText('In circulation')).toBeInTheDocument(); - expect(screen.getByText('Government bank')).toBeInTheDocument(); - expect(screen.getByText('DNB')).toBeInTheDocument(); + expect(screen.getByText('Government reserve')).toBeInTheDocument(); + expect(screen.getByText(Fmt.formatUnits('7500000'))).toBeInTheDocument(); + expect(screen.getByText(/pays coupon, buyback, redemption/)).toBeInTheDocument(); // Allowlist entries show their live WNOK holding; a failed chain // read (null) renders an em dash instead of a number. The holding diff --git a/services/nb-ui/tests/CouponPayoutPage.test.jsx b/services/nb-ui/tests/CouponPayoutPage.test.jsx index 44b5876..576d5ea 100644 --- a/services/nb-ui/tests/CouponPayoutPage.test.jsx +++ b/services/nb-ui/tests/CouponPayoutPage.test.jsx @@ -234,7 +234,7 @@ describe('CouponPayoutPage', () => { // A partial allocation leaves unsold units on the BondManager itself. // The contract requires covering EVERY holder, so the preview keeps // the manager row (labelled) and warns that the payout will fail on - // the government TBD allowlist. Mixed casing proves the address + // the WNOK allowlist. Mixed casing proves the address // match is case-insensitive. const MANAGER = '0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc'; const bond = { @@ -253,9 +253,7 @@ describe('CouponPayoutPage', () => { expect(screen.getAllByRole('row')).toHaveLength(4); expect(screen.getByText('(treasury)')).toBeInTheDocument(); expect(screen.getByText(/treasury-held units/i)).toBeInTheDocument(); - expect( - screen.getByText(/will fail unless the manager is explicitly allowlisted/), - ).toBeInTheDocument(); + expect(screen.getByText(/will fail unless the manager is allowlisted/)).toBeInTheDocument(); // Total covers ALL holders — the full cash leg the contract demands: // (600 + 400) units × 4.25% of face = 42.50 K NOK. expect(screen.getByText('42.50 K NOK')).toBeInTheDocument(); From 91bdd0d596eb2c5e0cb882c674e5178b145e7539 Mon Sep 17 00:00:00 2001 From: greaker <152405392+Greaker@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:11:44 +0200 Subject: [PATCH 2/3] docs: describe the wNOK reserve leg; accept ADR 0004 Architecture, the auction and coupon/redemption sequence diagrams, and the contracts overview now say every bond cash leg settles in WNOK against the government reserve account fixed on BondManager. The two known-issue entries rooted in the old TBD leg are rewritten: the treasury-held-units deadlock now sits on the WNOK allowlist with the Central Bank page as the workaround, and the hard-coded settlement bank becomes the immutable reserve account with a narrowed follow-up. The closed-loop plan's Phase 6 carries a note that coupon and redemption already settle in wNOK. ADR 0004 moves to Accepted in the ADR index and the documentation index. Plan progress in docs/plans/bond-cash-leg-wnok/progress.md. --- docs/ARCHITECTURE.md | 10 ++-- docs/DOCUMENTATION_INDEX.md | 2 +- docs/KNOWN_ISSUES.md | 48 ++++++++----------- .../0004-settle-all-bond-cash-legs-in-wnok.md | 2 +- docs/decisions/README.md | 2 +- docs/diagrams/processes/auction-sequence.md | 4 +- .../processes/coupon-redemption-sequence.md | 17 +++---- docs/plans/bond-cash-leg-wnok/progress.md | 8 ++-- ...oop-settlement-and-omnibus-custody-plan.md | 2 +- 9 files changed, 46 insertions(+), 49 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 48646d3..53b761c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -123,16 +123,18 @@ Foundry. Key components include: - predeployed in the Besu genesis so its address stays stable in the local sandbox - bond lifecycle contracts under `contracts/src/norges-bank/` - - `BondManager`: issuer-controlled entrypoint for creating bonds and auctions - and for finalising auctions with DvP settlement + - `BondManager`: issuer-controlled entrypoint for creating bonds and auctions, + finalising auctions with DvP settlement, and paying coupons, buybacks, and + redemptions; every cash leg settles in `Wnok` against one government + reserve account fixed at deployment (`GOV_RESERVE`), see ADR 0004 - `BondAuction`: sealed-bid auction contract that accepts encrypted bids and publishes allocations during finalisation - `BondToken`: partitioned bond token keyed by ISIN - `BondDvP`: settlement component that coordinates the cash leg against the bond leg - cash-side token - - `Wnok` (`contracts/src/norges-bank/Wnok.sol`): mock cash token used for - local settlement flows + - `Wnok` (`contracts/src/norges-bank/Wnok.sol`): mock central-bank cash token + used for every bond cash leg and for local settlement flows > **Planned — decided, not yet implemented:** this ERC-1410-based security-token > design is slated to migrate to canonical **ERC-3643 (T-REX)** — a shared on-chain diff --git a/docs/DOCUMENTATION_INDEX.md b/docs/DOCUMENTATION_INDEX.md index 4db09c9..b3902a9 100644 --- a/docs/DOCUMENTATION_INDEX.md +++ b/docs/DOCUMENTATION_INDEX.md @@ -19,7 +19,7 @@ This file lists the documentation that is most likely to need updates when behav - `docs/decisions/0001-local-chain-besu-clique-london-baseline.md`: Why the local chain runs single-node Hyperledger Besu on Clique PoA pinned to the London EVM milestone with a zero-base-fee genesis — and the `PUSH0`, Foundry-fee, and transaction-only block-clock consequences. - `docs/decisions/0002-adopt-erc-3643-for-tokenized-securities.md`: Why securities (bonds + equities) move to canonical ERC-3643 (T-REX) with a shared identity registry + modular compliance, retiring the ERC-1410 partitioned `BondToken` and the bespoke ERC-20 `StockToken` — including the migration cost, cash (`Wnok`) staying on its own allowlist, and the redirect of the incremental-adoption plan. - `docs/decisions/0003-adopt-besu-qbft-osaka-with-archive-rpc.md`: Why the sandbox replaces Clique/London with Besu QBFT/Osaka, separates the validator from a Forest archive/RPC node, rejects a beacon client, and requires a destructive chain reset. - - `docs/decisions/0004-settle-all-bond-cash-legs-in-wnok.md`: Why every bond cash leg (issuance, buyback, coupon, redemption) settles in `Wnok` from a government reserve account, removing the government-nominated `Tbd` from the bond stack while keeping the allowance-based authorization for the closed-loop plan to replace. Proposed. + - `docs/decisions/0004-settle-all-bond-cash-legs-in-wnok.md`: Why every bond cash leg (issuance, buyback, coupon, redemption) settles in `Wnok` from a government reserve account, removing the government-nominated `Tbd` from the bond stack while keeping the allowance-based authorization for the closed-loop plan to replace. Accepted. - `docs/plans/README.md`: How implementation plans are laid out — new plans are folders with `intent.md`, `design.md`, `plan.md`, and a committed `progress.md`; existing single-file plans stay as they are. - `docs/plans/bond-cash-leg-wnok/`: Plan folder (intent, design, plan, progress) to pay bond coupon, buyback, and redemption in wNOK from the government reserve account instead of the government-nominated TBD, so primary dealers buy and are paid in central-bank money; covers `BondManager`, deploy wiring, tests, the Central Bank API field and UI, and the two related known issues. - `docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md`: Plan to settle the primary auction cash leg by central-bank authority instead of a bidder ERC-20 allowance (fixes the `ERC20InsufficientAllowance` / `FailureReason.Cash` finalisation failure), then a phased path to omnibus broker custody, a unified `PrimaryDealerRegistry`, and two-tier coupon/redemption. Phase 1 is diff-level (Roles + Wnok `settle()` + BondDvP cash-leg swap + deploy wiring); Phases 2–6 are roadmap. diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index f15cdfd..0744c54 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -45,17 +45,16 @@ (`CouponPaymentBalanceMismatch` / the redeem zero-supply check), so the manager cannot be skipped; - including the manager fails the cash leg with - `SettlementFailure(AllowlistViolation)` because the government - settlement TBD's allowlist (correctly) does not include the manager - contract. + `SettlementFailure(AllowlistViolation)` because the WNOK allowlist + (correctly) does not include the manager contract. - The API surfaces these reverts as readable 409 details (nested custom errors decoded, with a treasury-specific hint), and the payout modal flags treasury-held units with a warning before the transaction is attempted. -- Sandbox workarounds: allowlist the BondManager on the government TBD - via the Banking page (the payment then succeeds; the treasury's own - coupon cash accrues to the manager contract), or use fully-allocated - auctions when coupon/redemption flows are being tested. +- Sandbox workarounds: allowlist the BondManager on WNOK via the Central + Bank page (the payment then succeeds; the treasury's own coupon cash + accrues to the manager contract), or use fully-allocated auctions when + coupon/redemption flows are being tested. - Planned follow-up (contract-side decision): burn unsold units at finalisation, or skip self-held units in `payCoupon` / `redeem` on-chain. @@ -336,23 +335,18 @@ visible at a glance. verify both `nb-bond-api` and `nb-ui` build. The Dependabot PRs were closed (not merged) with this rationale. -## BondManager hard-codes the government settlement bank (`GOV_TBD`) -- `BondManager` stores the government's cash-leg settlement bank as an `immutable` - — `GOV_TBD`, with `_GOV_RESERVE` derived from `ITbd(GOV_TBD).govReserve()` - (`contracts/src/norges-bank/BondManager.sol:46-94`). It is the TBD (tokenized - bank deposit) whose tokens settle bond coupon and redemption payments - (`payCoupon`, `redeem`); in the local sandbox it is wired to Nordea - (`TBD_NORDEA_CONTRACT_NAME`, resolved from GlobalRegistry at deploy — - `contracts/script/norges-bank/10_Bond.s.sol:26`, `11_BondSetup.s.sol:32`). -- Because it is `immutable`, switching the government's agent bank requires - redeploying `BondManager` and re-wiring the bond stack. The source of truth is - also split: the deploy resolves the bank from a GlobalRegistry name, then freezes - it on `BondManager`. This is fine for the single-bank sandbox but is the wrong - long-term home — "who settles government cash" is a settlement / governance - concern, not a bond-contract detail. -- Planned follow-up: move the designation to mutable, well-modelled state — either a - stable, resolvable GlobalRegistry entry (e.g. a `Gov TBD` name) or the settlement - layer / `PrimaryDealerRegistry` introduced by - [`docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md`](plans/closed-loop-settlement-and-omnibus-custody-plan.md), - so the agent bank can change without a redeploy. The operator Central Bank page - surfaces the current value by reading `BondManager.GOV_TBD()`. +## BondManager fixes the government reserve account at deployment (`GOV_RESERVE`) +- `BondManager` stores the government reserve account as an `immutable`, + `GOV_RESERVE` (`contracts/src/norges-bank/BondManager.sol`). It is the WNOK + account that receives issuance proceeds and pays buyback, coupon, and + redemption cash (ADR 0004). In the local sandbox it is the fixture reserve + key (`PK_GOV_RESERVE`), passed by `contracts/script/norges-bank/10_Bond.s.sol`. +- Because it is `immutable`, changing the reserve account requires redeploying + `BondManager` and re-wiring the bond stack. This is fine for the sandbox, but + "which account pays the state's obligations" is a governance concern that may + deserve mutable, role-gated state. +- Planned follow-up: decide whether the designation becomes mutable (admin + setter with its own ADR) or moves to the settlement layer introduced by + [`docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md`](plans/closed-loop-settlement-and-omnibus-custody-plan.md). + The operator Central Bank page surfaces the current account and its WNOK + balance by reading `BondManager.GOV_RESERVE()`. diff --git a/docs/decisions/0004-settle-all-bond-cash-legs-in-wnok.md b/docs/decisions/0004-settle-all-bond-cash-legs-in-wnok.md index 64b4a5f..f8c1490 100644 --- a/docs/decisions/0004-settle-all-bond-cash-legs-in-wnok.md +++ b/docs/decisions/0004-settle-all-bond-cash-legs-in-wnok.md @@ -1,6 +1,6 @@ # 0004. Settle every bond cash leg in wNOK from a government reserve account -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-09-07 - **Deciders:** sandbox operator - **Tags:** contracts, cash-settlement, wnok, bonds diff --git a/docs/decisions/README.md b/docs/decisions/README.md index f6855ab..2d53a6f 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -33,4 +33,4 @@ ADRs **complement** the other docs, they do not replace them: | [0001](0001-local-chain-besu-clique-london-baseline.md) | Local chain runs Hyperledger Besu on a Clique + London baseline | Superseded by 0003 | | [0002](0002-adopt-erc-3643-for-tokenized-securities.md) | Adopt ERC-3643 (T-REX) for tokenized securities, retiring the ERC-1410 model | Accepted | | [0003](0003-adopt-besu-qbft-osaka-with-archive-rpc.md) | Adopt Besu QBFT and Osaka with a separate archive/RPC node | Accepted | -| [0004](0004-settle-all-bond-cash-legs-in-wnok.md) | Settle every bond cash leg in wNOK from a government reserve account | Proposed | +| [0004](0004-settle-all-bond-cash-legs-in-wnok.md) | Settle every bond cash leg in wNOK from a government reserve account | Accepted | diff --git a/docs/diagrams/processes/auction-sequence.md b/docs/diagrams/processes/auction-sequence.md index 6209ab6..b1050fb 100644 --- a/docs/diagrams/processes/auction-sequence.md +++ b/docs/diagrams/processes/auction-sequence.md @@ -16,7 +16,7 @@ sequenceDiagram participant BT as BondToken actor Bidder as Primary dealer participant DVP as BondDvP - participant Cash as WNOK or government TBD + participant Cash as WNOK Operator->>UI: Create bond or schedule auction UI->>API: POST /v1/bonds or POST /v1/bonds/{isin}/auctions @@ -81,7 +81,7 @@ sequenceDiagram end else BUYBACK loop Each allocation - BM->>DVP: settle bond burn + government TBD payment + BM->>DVP: settle bond burn + government reserve WNOK payment DVP->>BT: buybackRedeemFor(bidder) DVP->>Cash: transferFrom(government reserve, bidder) end diff --git a/docs/diagrams/processes/coupon-redemption-sequence.md b/docs/diagrams/processes/coupon-redemption-sequence.md index 47c3c72..b5550f8 100644 --- a/docs/diagrams/processes/coupon-redemption-sequence.md +++ b/docs/diagrams/processes/coupon-redemption-sequence.md @@ -1,8 +1,9 @@ # Coupon and Redemption Sequence -Coupon and redemption cash is the government-nominated TBD, paid from its -configured government reserve. The API derives holders from the chain -projection unless the operator supplies an explicit list. +Coupon and redemption cash is WNOK, paid from the government reserve account +fixed on `BondManager` at deployment (`GOV_RESERVE`). Payouts move existing +WNOK; nothing is minted. The API derives holders from the chain projection +unless the operator supplies an explicit list. ```mermaid sequenceDiagram @@ -14,7 +15,7 @@ sequenceDiagram participant BM as BondManager participant BT as BondToken participant DVP as BondDvP - participant TBD as Government-nominated TBD + participant WNOK as Wnok actor Holder as Bond holder Note over Operator,BT: RATE auction has enabled the bond,
set yield, and started its maturity timer @@ -30,8 +31,8 @@ sequenceDiagram loop Every supplied holder with balance BM->>BT: balanceOfByPartition(partition, holder) BM->>DVP: settle cash-only coupon - DVP->>TBD: transferFrom(government reserve, holder, amount) - TBD-->>Holder: Tokenized-deposit balance increases + DVP->>WNOK: transferFrom(government reserve, holder, amount) + WNOK-->>Holder: WNOK balance increases end BM->>BT: Verify processed balance equals total supply @@ -52,8 +53,8 @@ sequenceDiagram BM->>BT: balanceOfByPartition(partition, holder) BM->>DVP: settle redemption
nominal cash + bond burn DVP->>BT: redeemFor(holder, isin, balance, operator) - DVP->>TBD: transferFrom(government reserve, holder, nominal) - TBD-->>Holder: Tokenized-deposit balance increases + DVP->>WNOK: transferFrom(government reserve, holder, nominal) + WNOK-->>Holder: WNOK balance increases end BM->>BT: Require partition totalSupply == 0 diff --git a/docs/plans/bond-cash-leg-wnok/progress.md b/docs/plans/bond-cash-leg-wnok/progress.md index 36a328f..5dafeb9 100644 --- a/docs/plans/bond-cash-leg-wnok/progress.md +++ b/docs/plans/bond-cash-leg-wnok/progress.md @@ -1,9 +1,9 @@ # Bond cash leg in wNOK — Progress **Plan:** [`plan.md`](plan.md) -**Last updated:** 2026-09-08 — Phases 2 and 3 done on `feature/bond-cash-leg-wnok-api` (stacked on PR #272): Central Bank resource exposes `govReserve`, payouts and finalisation push live changes, UI updated -**Current phase:** Phase 4: Fresh local sandbox validation (needs operator go-ahead to delete the local sandbox) -**Next action:** After PR #272 merges, rebase `feature/bond-cash-leg-wnok-api` onto `development`; run Phase 4 on a fresh sandbox; then Phase 5 docs and open PR 2 +**Last updated:** 2026-09-08 — Phases 2, 3, and 5 done on `feature/bond-cash-leg-wnok-api`; Phase 4 (fresh local sandbox run) in progress +**Current phase:** Phase 4: Fresh local sandbox validation (operator approved `./sandbox.sh start` on 2026-09-08; no existing sandbox to delete) +**Next action:** Finish the Phase 4 lifecycle run and record its evidence here; after PR #272 merges, rebase onto `development` and open PR 2; archive this folder once PR 2 merges ## Phase Log @@ -14,7 +14,7 @@ | 2 — API contract and revert decoding | Done | `govReserve {address, wnokBalance}` replaces `govSettlementBank`; `openapi.json` regenerated (one resource changed); coupon, redemption, and finalisation decode with the WNOK ABI and publish `bidders` + `central-bank`; nb-bond-api lint, format, build, 245 jest tests green (2026-09-08) | PR 2 | | 3 — UI alignment | Done | Central Bank KPI shows the reserve's WNOK balance and address; coupon warning names the WNOK allowlist; nb-ui format, lint, build, 121 vitest tests green (2026-09-08) | PR 2 | | 4 — Fresh local sandbox validation | Not started | | | -| 5 — Architecture docs, known issues, index | Not started | | | +| 5 — Architecture docs, known issues, index | Done | `ARCHITECTURE.md`, both process diagrams, two known-issue entries rewritten, closed-loop plan Phase 6 note, ADR 0004 set to Accepted in the ADR index and documentation index; hygiene and link checks green (2026-09-08) | PR 2 | ## Deviations From the Plan diff --git a/docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md b/docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md index bf2d1de..7ed74c8 100644 --- a/docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md +++ b/docs/plans/closed-loop-settlement-and-omnibus-custody-plan.md @@ -206,7 +206,7 @@ PrimaryDealerRegistry (Phase 2), any custody/broker change (Phase 3+), coupon/re **Phase 5 — Secondary venue refactor.** Replace the holder-level `BondOrderBook` (which presumes segregated on-chain balances, incompatible with omnibus) with a **broker-level venue**: same-broker fills = off-chain book entries; cross-broker fills = on-chain DvP with **EIP-712 signed orders + open-loop cash authorization** (the legitimately-open-loop leg that *does* use signatures/allowances). -**Phase 6 — Coupon & redemption.** Coupon: CB debits the gov reserve (authority) → the gov's designated bank mints **backed** TBD (`Tbd._mintFromGovReserve` already backs it) → distributes **per broker** (fan-out scales) → brokers credit clients off-chain. Redemption at maturity: reverse DvP via `redeemFor`/`buybackRedeemFor` (burn bond ↔ return principal in wNOK by authority). +**Phase 6 — Coupon & redemption.** _Note (2026-09-08): coupon, buyback, and redemption already settle in wNOK from the government reserve account (ADR 0004, `docs/plans/bond-cash-leg-wnok/`), so this phase starts from a single token; the TBD leg described below is superseded._ Coupon: CB debits the gov reserve (authority) → the gov's designated bank mints **backed** TBD (`Tbd._mintFromGovReserve` already backs it) → distributes **per broker** (fan-out scales) → brokers credit clients off-chain. Redemption at maturity: reverse DvP via `redeemFor`/`buybackRedeemFor` (burn bond ↔ return principal in wNOK by authority). --- From e0f9b47ded51da82b1b7c80b3a0eeb17461d13c4 Mon Sep 17 00:00:00 2001 From: greaker <152405392+Greaker@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:26:10 +0200 Subject: [PATCH 3/3] Record the fresh-sandbox lifecycle evidence for the wNOK cash leg Phase 4 of docs/plans/bond-cash-leg-wnok/: on a fresh local sandbox the deployed BondManager reports the fixture reserve as GOV_RESERVE with a max WNOK allowance for BondDvP, and a full issuance, buyback, two-coupon, redemption lifecycle moves WNOK only between the reserve and the dealers with total supply unchanged. Two pre-existing observations are logged as follow-ups: the redemption route returns a 500 for a nested revert the coupon route would decode into a 409, and time-gated calls are estimated against the latest block's timestamp. --- docs/plans/bond-cash-leg-wnok/progress.md | 38 ++++++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/plans/bond-cash-leg-wnok/progress.md b/docs/plans/bond-cash-leg-wnok/progress.md index 5dafeb9..b2f33b6 100644 --- a/docs/plans/bond-cash-leg-wnok/progress.md +++ b/docs/plans/bond-cash-leg-wnok/progress.md @@ -1,9 +1,9 @@ # Bond cash leg in wNOK — Progress **Plan:** [`plan.md`](plan.md) -**Last updated:** 2026-09-08 — Phases 2, 3, and 5 done on `feature/bond-cash-leg-wnok-api`; Phase 4 (fresh local sandbox run) in progress -**Current phase:** Phase 4: Fresh local sandbox validation (operator approved `./sandbox.sh start` on 2026-09-08; no existing sandbox to delete) -**Next action:** Finish the Phase 4 lifecycle run and record its evidence here; after PR #272 merges, rebase onto `development` and open PR 2; archive this folder once PR 2 merges +**Last updated:** 2026-09-08 — Phases 2 to 5 done on `feature/bond-cash-leg-wnok-api`; fresh-sandbox lifecycle run recorded +**Current phase:** All phases done; waiting on PR #272 to merge before PR 2 +**Next action:** After PR #272 merges, rebase `feature/bond-cash-leg-wnok-api` onto `development`, rerun both package gates, open PR 2; archive this folder with `Status: Implemented` once PR 2 merges ## Phase Log @@ -13,11 +13,13 @@ | 1 — BondManager cutover, deploy wiring, contract docs, ADR | Done (PR #272 open, all checks green) | `forge build`, `forge test`: 25 suites, 393 passed (3 new cases); verify-mapping check passed; `grep private-bank` on `BondManager.sol` empty; nb-bond-api lint, format, and 244 jest tests green with the refreshed ABI (2026-09-08) | PR 1 | | 2 — API contract and revert decoding | Done | `govReserve {address, wnokBalance}` replaces `govSettlementBank`; `openapi.json` regenerated (one resource changed); coupon, redemption, and finalisation decode with the WNOK ABI and publish `bidders` + `central-bank`; nb-bond-api lint, format, build, 245 jest tests green (2026-09-08) | PR 2 | | 3 — UI alignment | Done | Central Bank KPI shows the reserve's WNOK balance and address; coupon warning names the WNOK allowlist; nb-ui format, lint, build, 121 vitest tests green (2026-09-08) | PR 2 | -| 4 — Fresh local sandbox validation | Not started | | | +| 4 — Fresh local sandbox validation | Done | Fresh `./sandbox.sh start` on the feature branch; `GOV_RESERVE()` equals the fixture reserve, `WNOK()` the registered token, reserve→BondDvP allowance is max, reserve allowlisted; full lifecycle through the API with the balance trail below (2026-09-08) | PR 2 | | 5 — Architecture docs, known issues, index | Done | `ARCHITECTURE.md`, both process diagrams, two known-issue entries rewritten, closed-loop plan Phase 6 note, ADR 0004 set to Accepted in the ADR index and documentation index; hygiene and link checks green (2026-09-08) | PR 2 | ## Deviations From the Plan +- Phase 4 drove the lifecycle through the NB Bond API's bidder routes instead of the reference + bid CLI, and needed no `./sandbox.sh delete` because no sandbox for this repository existed. - Phase 1: the `GovReserveAddressZero` constructor test lives in a second, tiny test contract at the end of `BondManager.t.sol`. A second `new BondManager` inside `BondManagerTest` makes solc 0.8.36 (via-IR) fail with "Tag too large for reserved space", so the large test @@ -50,9 +52,27 @@ - The API degrades `govSettlementBank` to `null` when the manager call fails — verified in `services/nb-bond-api/src/app.ts` on 2026-09-07. +## Phase 4 Balance Trail (local sandbox, 2026-09-08) + +Bond `NO0000000WNK`, 100 units at 1000 WNOK nominal, RATE auction cleared at 425 bps +(coupon per unit 42 after integer truncation), maturity 2 coupon periods of 60 s. + +| Step | Reserve WNOK | Dealer A WNOK | Dealer B WNOK | WNOK supply | Bond supply | +|---|---|---|---|---|---| +| Baseline | 10 000 000 | 1 200 000 | 1 100 000 | 12 300 000 | 0 | +| Issuance (A 60 units, B 40 units) | 10 100 000 | 1 140 000 | 1 060 000 | 12 300 000 | 100 | +| Buyback (A sells 30 units at 98.00) | 10 070 600 | 1 169 400 | 1 060 000 | 12 300 000 | 70 | +| Coupon 1 (70 units × 42) | 10 067 660 | 1 170 660 | 1 061 680 | 12 300 000 | 70 | +| Coupon 2, bond matured | 10 064 720 | 1 171 920 | 1 063 360 | 12 300 000 | 70 | +| Redemption (70 units × 1000) | 9 994 720 | 1 201 920 | 1 103 360 | 12 300 000 | 0 | + +Every cash movement is WNOK between the reserve and the dealers; WNOK supply never changes; +no TBD balance is involved. The Central Bank resource reported the reserve balance at each +step and the Banking page's TBD listing was unaffected. + ## Blocked / Waiting On -- Phase 4 needs an explicit go-ahead to delete and recreate the local sandbox. +- PR #272 merge (operator), then rebase and open PR 2. ## Follow-ups Found Along the Way @@ -62,6 +82,14 @@ - Test fixtures allowlist `BondManager` on wNOK with a stale comment while the deploy script does not — `contracts/test/norges-bank/BondManager.t.sol` setup — align once the treasury-held-units contract fix is decided. +- The redemption route maps an unknown nested revert (here `NotMatured` inside + `SettlementFailure`) to a 500 with raw calldata, while the coupon route decodes it into a + 409 — `services/nb-bond-api/src/app.ts` redemption handler — pre-existing; give redemption + the same `describeRevert` treatment. +- Time-gated calls (`payCoupon`, `redeem`) are gas-estimated against the latest block, whose + timestamp only advances when a transaction is mined, so a coupon that is due by wall clock + can still fail `CouponNotReady` until any transaction lands — sandbox chain behaviour + already noted in ADR 0001's consequences; consider a note in the API's 409 detail or docs. - Treasury-held units still deadlock coupon payout, now on the wNOK allowlist — existing entry in `docs/KNOWN_ISSUES.md`; contract-side fix (burn or skip self-held units) remains open.