Skip to content

feat: billing contract v1 (WIP-107)#830

Draft
kilianglas wants to merge 24 commits into
mainfrom
feat/billing-contract-v1
Draft

feat: billing contract v1 (WIP-107)#830
kilianglas wants to merge 24 commits into
mainfrom
feat/billing-contract-v1

Conversation

@kilianglas

@kilianglas kilianglas commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

First version of the on-chain Billing Contract (WIP-107): OPRF nodes count unique RP requests per ~epoch, submit EIP-712-signed votes, and the contract finalizes the lower-median count (gated on quorum), prices it through a tiered WLD fee schedule, accrues per-RP debt, and surfaces is_blocked for RPs past their payment window.

Scope of this PR: contract + IBillingContract interface + Foundry tests only. No deploy script and no OPRF node-side Rust (separate follow-up entries).

Deviations from initial spec

  • Voting is chunked to allow for larger numbers of RPs
  • Finalization of and epoch is an async/ separate operation

Comment thread contracts/src/core/BillingContract.sol Outdated
// Members //
////////////////////////////////////////////////////////////

// DO NOT REORDER! To ensure compatibility between upgrades, it is exceedingly important

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should we leave a gap variable for future updates of this contract?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this only make sense if another contracts inherits from BillingContract?

Kemperino
Kemperino previously approved these changes Jul 6, 2026
* `maxAmount` guard. Pulls WLD from msg.sender to the fee recipient.
* @param payments The per-RP payment instructions.
*/
function pay(RpPayment[] calldata payments) external;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. i feel like collectDebt would be better naming considering all the accounting logic happening on this call?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? pay seems to be the correct name here.

// Members //
////////////////////////////////////////////////////////////

// DO NOT REORDER! To ensure compatibility between upgrades, it is exceedingly important

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this only make sense if another contracts inherits from BillingContract?

Comment thread contracts/src/core/interfaces/IBillingContract.sol
Comment thread contracts/src/core/BillingContract.sol Outdated
/// that ever existed keeps its true boundaries and windows: epoch `e` is governed by the
/// newest era with `startEpoch <= e`, so timing changes never affect existing epochs.
/// Grows by one entry per {setTiming} call (rare, owner-only), never per epoch.
Era[] internal _eras;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth discussing to change to something like the following, then we have future compatibility to change the Era struct.

mapping(uint32 => Era) internal _eras;
uint32 internal _eraCount;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like if we need to update this, we can just deploy a new contract version. Not sure if taking on extra complexity here is worth it.

Comment thread contracts/src/core/BillingContract.sol
Comment thread contracts/src/core/interfaces/IBillingContract.sol Outdated
Comment thread contracts/src/core/BillingContract.sol
Comment thread contracts/src/core/BillingContract.sol Outdated
@kilianglas kilianglas force-pushed the feat/billing-contract-v1 branch from 3a018b7 to 88e7b55 Compare July 6, 2026 16:58
@kilianglas kilianglas force-pushed the feat/billing-contract-v1 branch from 88e7b55 to 7336788 Compare July 6, 2026 17:04
Co-authored-by: kempy <33121795+Kemperino@users.noreply.github.com>
kilianglas and others added 11 commits July 7, 2026 12:05
Add permissionless view getters so off-chain keepers can decide whether
finalization is pending without blind-firing transactions:

- latestClosedEpoch(): the latest epoch whose voting window has closed.
- latestFinalizedEpoch(): the latest fully-finalized epoch.

Both are inclusive high-water marks returning (bool exists, uint32 epoch),
so the "nothing yet" genesis case is unambiguous and the pending range is a
clean [latestFinalized+1, latestClosed].

Co-authored-by: Cursor <cursoragent@cursor.com>
Add votingWindowEnd(epoch) and paymentWindowEnd(epoch) alongside epochEnd,
completing the per-epoch timing boundaries. Both derive from the era that
governs the window (the era of epoch+1), so they stay exact across setTiming
changes — unlike a client-side epochEnd + current-era-votingWindow estimate.

Off-chain keepers can use these directly: the finalizer for the voting-window
close, and the (upcoming) payer for the payment deadline.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread contracts/src/core/interfaces/IBillingContract.sol
Comment thread contracts/src/core/BillingContract.sol
* equal to type(uint256).max.
* @param tiers The new tier schedule.
*/
function setTierSchedule(Tier[] calldata tiers) external;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this shouldn't be in something like ITieredRebaedBillingContract.sol

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for setRebatePeriodEpochs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my point here is that we should leave in IBillingContract only the parts relevant to the core billing protocol - i.e. request counting, voting, etc.

how exactly fees are calculated is an "implementation detail" that's not relevant to e.g. oprf nodes

* @param votingWindow The voting window in seconds.
* @param paymentWindow The payment window in seconds.
*/
function setTiming(uint64 epochLength, uint64 votingWindow, uint64 paymentWindow) external;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo setter methods in general don't belong in this interface

Replace latestFinalizedEpoch and latestClosedEpoch with epochWatermarks so
keepers read both watermarks from the same block and avoid read skew between
two separate calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

5 participants