Difficulty: Expert. Scope: multi-week epic. Contract + SDK + dashboard.
Problem
MAX_RECIPIENTS: u32 = 32 (contracts/splitter/src/lib.rs) hard-caps a split at 32 recipients, and payout() iterates every recipient inside a single transaction. That makes the whole class of large-scale distributions impossible: a 5,000-address airdrop, a token-holder revenue share, a creator paying 400 contributors. There is no path to it today, and raising the constant does not work because the per-transaction resource budget cannot pay 5,000 transfers in one call.
Proposal
Add a second split kind: a merkle-claim split. The creator commits a merkle root of (recipient, share) leaves; recipients (or anyone on their behalf) call claim(id, token, recipient, share, proof) to pull their portion of a distribution. This turns an O(N) push into N independent O(log N) pulls.
Why this is hard
- Requires a new storage and event model that coexists with the existing push splits without breaking
pay, deposit, distribute, or nested Recipient::Split routing.
- Claim accounting must be exact across multiple funding rounds: a root can receive several deposits over time, and a claimant must be able to claim their cumulative entitlement without double claiming. Design the "claimed so far" bookkeeping (per epoch, or cumulative-amount merkle a la Uniswap MerkleDistributor).
- Must preserve the protocol invariant that amount in equals amount claimable out, including rounding and dust, matching the existing
amounts() rules.
- Interaction with nested splits: can a merkle leaf itself be a
Recipient::Split? Decide and enforce.
- SDK must generate roots and proofs deterministically and match the on-chain hashing exactly (byte-for-byte leaf encoding), or every claim fails.
Deliverables
- Design RFC first (see acceptance).
- Contract:
create_merkle_split, fund (deposit against a root), claim with proof verification, is_claimed, and events.
- SDK: leaf encoding, root construction, proof generation, and a typed
claim helper.
- Dashboard: a claim page where a recipient sees and claims their entitlement.
- Full test suite including a large-tree fixture.
Acceptance criteria
- A design RFC is reviewed and signed off by a maintainer before implementation (leaf encoding, claim accounting model, dust handling, nested-leaf decision).
- A single root supports at least several thousand recipients, provable by test.
- Claiming is exact and idempotent: no double claim, cumulative funding handled, conservation holds (funded == sum of claimable).
- On-chain leaf hashing and SDK leaf hashing are proven identical by a cross-check test.
- Gas/resource cost of a claim is bounded and documented (coordinate with the cost-benchmark epic).
- Docs cover when to use a merkle split vs a push split.
Related
Complements, does not replace, push splits. Interacts with #260 (claimable fallback) and #264 (distribute_all_tokens); reconcile the claim model with those.
Difficulty: Expert. Scope: multi-week epic. Contract + SDK + dashboard.
Problem
MAX_RECIPIENTS: u32 = 32(contracts/splitter/src/lib.rs) hard-caps a split at 32 recipients, andpayout()iterates every recipient inside a single transaction. That makes the whole class of large-scale distributions impossible: a 5,000-address airdrop, a token-holder revenue share, a creator paying 400 contributors. There is no path to it today, and raising the constant does not work because the per-transaction resource budget cannot pay 5,000 transfers in one call.Proposal
Add a second split kind: a merkle-claim split. The creator commits a merkle root of
(recipient, share)leaves; recipients (or anyone on their behalf) callclaim(id, token, recipient, share, proof)to pull their portion of a distribution. This turns an O(N) push into N independent O(log N) pulls.Why this is hard
pay,deposit,distribute, or nestedRecipient::Splitrouting.amounts()rules.Recipient::Split? Decide and enforce.Deliverables
create_merkle_split,fund(deposit against a root),claimwith proof verification,is_claimed, and events.claimhelper.Acceptance criteria
Related
Complements, does not replace, push splits. Interacts with #260 (claimable fallback) and #264 (distribute_all_tokens); reconcile the claim model with those.