Skip to content

Fix the coupon-closure review findings; keep zero-supply bonds closable - #278

Merged
Greaker merged 4 commits into
developmentfrom
feature/coupon-closure-review-fixes
Sep 14, 2026
Merged

Greaker merged 4 commits into
developmentfrom
feature/coupon-closure-review-fixes

Conversation

@Greaker

@Greaker Greaker commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fixes the ten findings from the review of Settle bond coupon, buyback, and redemption in wNOK #272–Archive the redeem-with-final-coupon plan; accept ADR 0005 #277 (plan: docs/plans/coupon-closure-review-fixes/). Contracts: withdrawFailedIssuance removed so unsold manager-held units can never become paid holdings; payCoupon rejects a repeated holder (DuplicateHolder); a per-period CouponPeriodPaid event lets the projection advance the payment count even when nobody is paid; structs moved to the types block; the two dealer accounts are seeded on the TBD Nordea allowlist again (in 08_TbdSetup.s.sol).
  • API: the coupon route accepts an empty holder set when the projected supply is zero, so a bond bought back to zero can still be closed; HoldersBody rejects duplicates and drops its redemption wording; CouponPeriodPaid → COUPON_PERIOD_PAID history entry plus the coupon-paid transition; an issued bond with remaining coupon periods stays outstanding regardless of supply; the write-only redemptionComplete flag is dropped from the projected state (column retained); /v1/health gains contracts.bondManagerCompatible from a GOV_RESERVE() probe and logs an error on skew.
  • UI: the pay-coupon preview uses the contract's integer per-unit coupon in BigInt WNOK (42 per unit at 425 bps, not 42.5) and compares the reserve balance as BigInt; the payout queue lists issued bonds that still owe periods, with copy for the bought-back case. Docs: known-issue entry for the WNOK allowlist requirement on every holder; contract reference, auction-sequence note, and NatSpec pages.

Test plan

  • cd contracts && forge fmt --check && forge build --sizes && bash ./check-verify-latest-mapping.sh && forge test — 25 suites, 395 tests (duplicate-with-omission, period event, empty-holder closure of a bought-back bond)
  • cd services/nb-bond-api && npm run lint && npm run format:check && npm test && npm run build — 248 tests (composer status rows, duplicate body, health field)
  • cd services/nb-ui && npm run format:check && npm run lint && npm test && npm run build — 125 tests (integer amounts, exact-reserve no warning, zero-supply bond queued)
  • python3 scripts/verification/check-public-repo-hygiene.py && python3 scripts/verification/check-markdown-links.py
  • Slither via Contracts CI (not runnable locally on arm64)
  • Optional fresh-sandbox run (full buyback then close with an empty holder set; health shows bondManagerCompatible: true) — needs a sandbox reset

Contracts: remove withdrawFailedIssuance, the only path that could turn
unsold manager-held units into paid holdings; reject a repeated holder in
payCoupon (DuplicateHolder) so a duplicate cannot be paid twice while
another holder is skipped; emit CouponPeriodPaid once per period so the
projected payment count advances even when nobody receives cash; move the
Period and PayoutTotals structs into the types block; seed the two dealer
accounts on the TBD Nordea allowlist again in the TBD setup script. forge
test: 25 suites, 395 passed, including an empty-holder closure of a bond
bought back to zero supply.

API: the coupon route accepts an empty holder set when the projected
supply is zero, HoldersBody rejects duplicates, CouponPeriodPaid drives
the coupon-paid transition and a COUPON_PERIOD_PAID history entry, an
issued bond with remaining periods stays outstanding regardless of
supply, the write-only redemptionComplete flag is dropped from the
projected state, and /v1/health reports contracts.bondManagerCompatible
from a GOV_RESERVE() probe. openapi.json regenerated; 248 jest tests.

UI: the pay-coupon preview uses the contract's integer per-unit coupon
in BigInt WNOK (42, not 42.5, at 425 bps) and compares the reserve as
BigInt; the payout queue lists issued bonds that still owe periods so a
bought-back bond can be closed. 125 vitest tests.

Docs: a known-issue entry for the WNOK allowlist requirement on every
holder, contract reference and NatSpec updates, and the plan folder
docs/plans/coupon-closure-review-fixes/.
The ingestion loop only fetches block timestamps for a fixed set of event
names. The new period event was not in that set, so its coupon-paid
transition projected lastCouponPayment as 0 and the next due time as the
coupon duration, which the fresh-sandbox run exposed. Add the event to
the prefetch filter.
A full projection replay applies a batch's manager events before its
token events, so IsinEnabled from the issuance block can arrive after
later coupon-period events and reset lastCouponPayment to the issuance
timestamp, which makes the next due time wrong until the next payment.
The enabled transition now seeds the schedule only while no coupon has
been reduced. Reducer test added; jest 249 green.
Phase 5 of docs/plans/coupon-closure-review-fixes/: health reports the
BondManager as compatible; a bond bought back to zero supply was paid
two periods with an empty holder set and closed with zero totals; a bond
whose only allocation failed advanced its projected payment count on the
interim period and closed by burning its 100 unsold units with the
reserve untouched; a duplicate-holder body was rejected with a 400. A
full projection replay projects the exact closing-block timestamps.
@Greaker
Greaker merged commit 1bf9e99 into development Sep 14, 2026
11 checks passed
@Greaker
Greaker deleted the feature/coupon-closure-review-fixes branch September 14, 2026 11:56
Greaker added a commit that referenced this pull request Sep 14, 2026
Move docs/plans/coupon-closure-review-fixes/ to docs/plans/archive/ with
its status set to Implemented (shipped via #278), tick its done criteria,
close out progress.md, and move the documentation-index entry to the
archive list.
@Greaker Greaker mentioned this pull request Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant