feat(solana): route epoch rent to the creator (ADR-0029) - #723
Conversation
`close_epoch` refunded the Epoch account's ~0.0664 SOL rent to whoever signed the close, not whoever paid to create it. Both instructions are permissionless, so a close-only bot could pocket rent funded by the operators doing the mandatory work. The program now records the funder in an auxiliary `EpochRentReceipt` PDA and refunds that account instead (gar #121). Client side: - `getEpochRentReceiptPDA` — hand-rolled derivation of `["epoch_rent_receipt", u64_le(epochIndex)]`. Codama generates no finder for this account: it is reached only through `remaining_accounts` and `admin_close_orphaned_epoch_rent_receipt`, so the IDL carries no seed metadata for it. - `createEpoch` attaches the receipt as a trailing `remaining_accounts` entry. It is not a declared account, so `create_epoch`'s IDL account list is unchanged and un-upgraded crankers keep working — they omit it and the epoch is created with `has_rent_receipt = 0`. - `closeEpoch` branches on `Epoch.hasRentReceipt` and appends `[receipt, creator]` (both writable) only when it is set. It deliberately mirrors the program in keying off program-controlled state rather than "did the caller pass a receipt?" — the latter would let a scavenger omit the receipt to force the legacy `close = payer` path and take the rent anyway. - `crankEpochStep` needs no change: it routes through `createEpoch` / `closeEpoch`, so it tolerates receipted and legacy epochs throughout the ~8-day transition window. Verified: the new PDA derivation matches an independent implementation of the program's seeds for staging epochs 772/779/780/781, on both the epoch and receipt addresses. Note: this depends on `@ar.io/solana-contracts` >= 1.2.0-staging.x for `EpochRentReceipt` and `Epoch.hasRentReceipt`. The dependency bump is intentionally NOT in this commit — the typed client is published only after the staging deploy is validated, per the ADR-0029 sequencing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKeNoHyV6m1Z3sh81Jykkf
1.2.0 is the first published client carrying EpochRentReceipt and Epoch.hasRentReceipt, generated from the contract source now deployed on mainnet (ario_gar slot 442893933, sha256 7f09326f…). The previous pin, 1.1.0, predates both and cannot decode a receipted epoch. Verified against the published package rather than the local build: the ADR-0029 surface is present, it still ships placeholder program IDs (so consumers must override per cluster, unchanged), tsc is clean and the full unit suite passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKeNoHyV6m1Z3sh81Jykkf
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe Solana client adds manual epoch rent receipt PDA derivation. Epoch creation passes the receipt PDA to the program. Epoch closure reads the receipt flag and passes receipt ownership accounts when required. Tests cover both account paths. The Solana contracts dependency is updated to ChangesEpoch rent receipt handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This PR routes rent for newly receipted epochs to the recorded creator while preserving legacy handling for older epochs. No actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant closeEpoch
participant fetchMaybeEpoch
participant fetchMaybeEpochRentReceipt
participant buildCloseEpochRentAccounts
participant SolanaProgram
closeEpoch->>fetchMaybeEpoch: Fetch epoch state
fetchMaybeEpoch-->>closeEpoch: Return hasRentReceipt
closeEpoch->>fetchMaybeEpochRentReceipt: Fetch receipt when flag is non-zero
fetchMaybeEpochRentReceipt-->>closeEpoch: Return creator
closeEpoch->>buildCloseEpochRentAccounts: Build remaining accounts
buildCloseEpochRentAccounts-->>closeEpoch: Return receipt and creator accounts
closeEpoch->>SolanaProgram: Send close_epoch instruction
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description clearly explains the problem, implementation, compatibility behavior, dependency update, verification results, and tests. It does not reproduce the repository checklist or provide an issue reference, but the required technical information is substantially complete. Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 4 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
❌ Your project status has failed because the head coverage (55.75%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## alpha #723 +/- ##
========================================
Coverage 55.74% 55.75%
========================================
Files 53 53
Lines 25056 25203 +147
Branches 858 861 +3
========================================
+ Hits 13968 14051 +83
- Misses 11063 11127 +64
Partials 25 25 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
🎉 This PR is included in version 4.3.0-alpha.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 4.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Client support for ADR-0029 —
close_epochnow refunds the account that created the epoch instead of whoever signs the close.Why
create_epochfunds the Epoch account (0.06637056 SOL) from whoever signs;close_epochrefunded that rent to whoever signed the close. Both are permissionless, so a close-only bot collected rent funded by operators doing the mandatory work. Measured on mainnet over a clean window (epochs 505–512): 8 of 8 closes taken by one address that created zero epochs — ~24.2 SOL/year.The contract side is
ario-garPR #121, already deployed to mainnet (slot 442893933,sha256 7f09326f…).Changes
getEpochRentReceiptPDA(pda.ts) +EPOCH_RENT_RECEIPT_SEED(constants.ts) — hand-rolled["epoch_rent_receipt", u64_le(index)]. Codama emits no finder for this account: it is reached only throughremaining_accountsandadmin_close_orphaned_epoch_rent_receipt, so the IDL carries no seed metadata.createEpochattaches the receipt as a trailingremaining_accountsentry (writable). Not a declared account, socreate_epoch's IDL account list is unchanged and un-upgraded crankers keep working — they omit it and the epoch is created withhas_rent_receipt = 0.closeEpochbranches onEpoch.hasRentReceiptand, only when set, appends[receipt, creator](both writable, in that order). It deliberately keys off program-controlled state, not "did the caller pass a receipt?" — the latter would let a scavenger omit the receipt to force the legacyclose = payerpath and take the rent anyway.crankEpochStepneeds no change: it routes throughcreateEpoch/closeEpoch, so it tolerates receipted and legacy epochs across the ~8-day transition.buildCloseEpochRentAccounts— the account-assembly decision extracted as a pure exported function, following the existingselectFinalizeGoneSwapOperatoridiom, becausecrank-epoch-step.test.tsstubscreateEpoch/closeEpochwholesale and cannot cover it.Verified on a real cluster (staging, then mainnet)
Not just unit tests — the full matrix ran against a deployed ADR-0029 program on staging:
close = payerEpochObservationsNotClosed (6081)MissingEpochRentReceipt (6093), and it does not fall through toclose = payercrankEpochStepcreates receipted epochsThe close was deliberately signed by a different key than the creator, so the result proves redirection rather than a self-refund.
Operational note for consumers
Because that
6093rejection is real, receipted epochs can only be closed by upgraded clients. An un-upgraded cranker attempting one fails insidecrankEpochStep's own catch — no wedge, nothing surfaced, the epoch simply stays open until an upgraded client closes it. Cranker and observer both pin^4.1.x, so any 4.x release satisfies them without a manifest change.Dependency
Requires
@ar.io/solana-contracts@^1.2.0forEpochRentReceiptandEpoch.hasRentReceipt. That version is published from the same contract source now live on mainnet.Tests
epoch-rent-receipt.test.ts— 11 tests: PDA determinism, per-index and per-program uniqueness, number/bigint equivalence, distinctness from the Epoch PDA, and the account-assembly matrix (flag clear → no extras; flag set →[receipt, creator]in order, both writable; any non-zero flag byte counts; diagnosable error when the flag is set but the receipt is missing).tsc --noEmit,biome check, and the full unit suite all pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01NKeNoHyV6m1Z3sh81Jykkf
Summary by CodeRabbit
New Features
Chores
Tests