Skip to content

feat(solana): route epoch rent to the creator (ADR-0029) - #723

Merged
vilenarios merged 2 commits into
alphafrom
feat/adr0029-epoch-rent-receipt
Aug 30, 2026
Merged

feat(solana): route epoch rent to the creator (ADR-0029)#723
vilenarios merged 2 commits into
alphafrom
feat/adr0029-epoch-rent-receipt

Conversation

@vilenarios

@vilenarios vilenarios commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Client support for ADR-0029 — close_epoch now refunds the account that created the epoch instead of whoever signs the close.

Why

create_epoch funds the Epoch account (0.06637056 SOL) from whoever signs; close_epoch refunded 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-gar PR #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 through remaining_accounts and admin_close_orphaned_epoch_rent_receipt, so the IDL carries no seed metadata.
  • createEpoch attaches the receipt as a trailing remaining_accounts entry (writable). 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, 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 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 across the ~8-day transition.
  • buildCloseEpochRentAccounts — the account-assembly decision extracted as a pure exported function, following the existing selectFinalizeGoneSwapOperator idiom, because crank-epoch-step.test.ts stubs createEpoch/closeEpoch wholesale 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:

receipt written, correct creator ✅ epochs 791–797
rent lands with the CREATOR on close +0.067567680 SOL (= 0.06637056 epoch + 0.00119712 receipt), closer paid only a 0.000005173 fee
receipt closed, no orphan
legacy epoch still closes via close = payer ✅ +0.066365458 to the closer
M8 gate still blocks close EpochObservationsNotClosed (6081)
un-upgraded cranker keeps working ✅ ran throughout, including on receipted epochs
old client cannot close a receipted epoch MissingEpochRentReceipt (6093), and it does not fall through to close = payer
crankEpochStep creates receipted epochs

The 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 6093 rejection is real, receipted epochs can only be closed by upgraded clients. An un-upgraded cranker attempting one fails inside crankEpochStep'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.0 for EpochRentReceipt and Epoch.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

    • Added support for tracking epoch rent receipts during epoch creation and closure.
    • Epoch closures now refund the recorded rent creator when applicable, while preserving legacy behavior for older epochs.
    • Added deterministic epoch rent receipt address derivation.
  • Chores

    • Updated the Solana contracts package to version 1.2.0.
  • Tests

    • Added coverage for rent receipt address derivation and close-epoch account handling.

vilenarios and others added 2 commits August 30, 2026 00:24
`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
@vilenarios
vilenarios requested a review from a team as a code owner August 30, 2026 14:15
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 976ec33e-ad01-4a80-a791-835926affdc6

📥 Commits

Reviewing files that changed from the base of the PR and between 397e456 and 8fbead0.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (5)
  • package.json
  • src/solana/constants.ts
  • src/solana/epoch-rent-receipt.test.ts
  • src/solana/io-writeable.ts
  • src/solana/pda.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The 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 1.2.0.

Changes

Epoch rent receipt handling

Layer / File(s) Summary
Receipt PDA contract
src/solana/constants.ts, src/solana/pda.ts, src/solana/epoch-rent-receipt.test.ts, package.json
Adds the EPOCH_RENT_RECEIPT_SEED constant and getEpochRentReceiptPDA. Updates @ar.io/solana-contracts to 1.2.0. Tests verify PDA derivation and program scoping.
Epoch creation wiring
src/solana/io-writeable.ts
createEpoch derives the receipt PDA and appends it as a writable remaining account.
Epoch closure account flow
src/solana/io-writeable.ts, src/solana/epoch-rent-receipt.test.ts
closeEpoch handles legacy epochs without receipts and receipted epochs with receipt and creator accounts. The helper validates missing receipt ownership and tests cover flag values and account ordering.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 8fbea

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
Loading

Suggested reviewers: atticusofsparta, fedellen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed 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 i…
Title check ✅ Passed The title clearly identifies the Solana feature and the main behavior change: routing epoch rent to the creator under ADR-0029.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

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 Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adr0029-epoch-rent-receipt

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.70470% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.75%. Comparing base (c6a73ea) to head (8fbead0).
⚠️ Report is 1 commits behind head on alpha.

Files with missing lines Patch % Lines
src/solana/io-writeable.ts 42.60% 66 Missing ⚠️

❌ 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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vilenarios
vilenarios merged commit 6ede82e into alpha Aug 30, 2026
12 checks passed
@atticusofsparta

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 4.3.0-alpha.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

@atticusofsparta

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 4.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants