@bananapus/referral-split-hook-v6
v0.0.9
Published
`@bananapus/referral-split-hook-v6` is a split hook that routes the fee project's reserved-token pool to referring projects' IVotes holders, in proportion to attributed fee volume. Same-chain referrers are pushed to a local `JBTokenDistributor`; cross-cha
Readme
Juicebox Referral Split Hook
@bananapus/referral-split-hook-v6 is a split hook that routes the fee project's reserved-token pool to referring projects' IVotes holders, in proportion to attributed fee volume. Same-chain referrers are pushed to a local JBTokenDistributor; cross-chain referrers are bridged through the fee project's sucker and settled atomically on their home chain. Credit on chains with no sucker pair is burned to the fee-project surplus rather than left to dilute existing holders.
Documentation
- ARCHITECTURE.md — module-level dataflow including the cross-chain path
- USER_JOURNEYS.md — five end-to-end flows from real callers' perspectives
- INVARIANTS.md — operational invariants enumerated per-contract
- RISKS.md — risk register and the burn-vs-defer-vs-revert design contract
- ADMINISTRATION.md — control posture, roles, recovery
- AUDIT_INSTRUCTIONS.md — where to look, what must hold
- SKILLS.md — quick-reference facts for AI agents working in this repo
- STYLE_GUIDE.md — repo-internal style ref
- CHANGELOG.md — version history
Overview
When a project pays a protocol fee, the caller can attribute that fee to a referral project via JBMultiTerminal.{cashOutTokensOf, sendPayoutsOf, useAllowanceOf}(..., referralProjectId). The referralProjectId parameter is encoded as (referralChainId << 48) | bareProjectId so cross-chain referrers can be credited from any source chain. JBTerminalStore records the per-terminal, per-(chainId, projectId) fee volume in feeVolumeByReferralOf and a denominator in totalFeeVolumeOf, normalizing all amounts to NATIVE_TOKEN 18-decimal units so multi-currency fee projects mix correctly.
This repo wires that volume ledger into the fee project's reserved-token split distribution:
- The fee project lists
JBReferralSplitHookas one of its reserved-token splits. - When the fee project calls
sendReservedTokensToSplitsOf, the hook receives its allocation of project tokens and books them intototalDeposited. - Routing depends on where the referrer lives:
- Same-chain referrer: anyone calls
pushTo(localChainId, refId)to forward the entitled delta intoJBTokenDistributor.fund(refToken, feeToken, amount). - Cross-chain referrer with a sucker pair: anyone calls
bridgeRemote(remoteChainId, refId, sucker, terminalToken)to cash out the entitled fee-project tokens through the sucker. The destination-side hook (same CREATE2 address) settles viaclaimAndPush(originChainId, refId, sucker, claimData), which mints fee-project tokens on the destination chain and forwards to the local distributor. - Cross-chain referrer with NO sucker pair: anyone calls
burnUnbridgeableCreditFor(remoteChainId, refId)to burn the entitled fee-project tokens. The bridged terminal-token value (already in the fee project's balance from the original protocol-fee flow) accrues pro-rata to all existing fee-token holders.
- Same-chain referrer: anyone calls
- Holders of the referring project's token claim their pro-rata stream over the distributor's configured vesting rounds.
The hook never custodies value beyond the brief window between receipt and forwarding. It never decides who is a "valid" referrer — it just resolves the volume ratio published by JBTerminalStore and routes.
Mental Model
- Fees attribute to referrers as they happen — recorded in
JBTerminalStorekeyed by(terminal, chainId, projectId), normalized to NATIVE units. - Reserved tokens accumulate on the fee project and periodically distribute via splits.
- One of those splits is this hook — it pools the tokens into
totalDeposited. - A permissionless
pushTo/bridgeRemote/burnUnbridgeableCreditForcall moves a referrer's pro-rata share to its destination — local distributor, remote sucker outbox, or burn. - Cross-chain bridges settle on the destination chain via
claimAndPush, which either forwards to the destination's local distributor or burns to the destination fee-project's surplus. - The referring project's IVotes holders claim from the distributor on the next vesting round.
Use this repo when the problem is "distribute fee-project tokens to referrers' holders, including cross-chain". Do not use it for the upstream volume attribution (that lives in nana-core-v6) or for the actual vesting/claim mechanics (that lives in nana-distributor-v6).
Key Contracts
| Contract | Role |
| --- | --- |
| JBReferralSplitHook | IJBSplitHook receiver for the fee project's reserved-token splits. Pools deposits, then routes per-referrer shares to (a) the local distributor, (b) the fee project's sucker outbox, or (c) the burn path. |
Read These Files First
src/interfaces/IJBReferralSplitHook.sol— every entrypoint with full NatSpecsrc/JBReferralSplitHook.sol— the contracttest/JBReferralSplitHook.t.sol— unit testsdeploy-all-v6/test/fork/ReferralRewardCrossChainFork.t.sol— full cross-chain E2E
Integration Traps
referralProjectIdis keyed to the referrer's HOME chain. AcrosspushTo,bridgeRemote,claimAndPush, andburnUnbridgeableCreditFor, this field ALWAYS refers to the projectId on the referrer's home chain — never to a numerically-matching projectId on some other chain. Projectid spaces are per-chain; project42on Optimism is unrelated to project42on Base.- Suckers need
MINT_TOKENSpermission. TheJBSuckerRegistry.deploySuckersForflow grantsDEPLOY_SUCKERSandMAP_SUCKER_TOKENbut NOTMINT_TOKENS. Without that grant,claimAndPushreverts insidesucker.claim→mintTokensOf. Grant explicitly viaJBPermissions.setPermissionsFor. - CREATE2 same-address-across-chains is load-bearing.
claimAndPushvalidates thatleaf.beneficiary == address(this)on the destination side. If the hook lives at a different address on the destination chain, every cross-chain claim reverts. - Multi-terminal deployments need multiple hooks.
TERMINALis constructor-set immutable. Fee volume on any other terminal is invisible to this hook. - Pro-rata is monotonic-but-not-snapshot-coherent. A late-arriving referrer who drives a lot of volume can reduce an earlier referrer's
entitledbelow their HWM. The hook clamps at zero; the residual stays in the pool. SeeRISKS.md§ 2 and § 7.4. - Burn-over-strand is the policy. When a leaf can't reach a recipient, the hook burns rather than holds. Cross-chain credit on a chain with no sucker → use
burnUnbridgeableCreditFor.claimAndPushto a chain whose local twin has no IVotes token → automatic burn inside the call. See RISKS.md § 7 for the matrix.
Where State Lives
- Per-referrer same-chain HWM:
pushedLocallyOf[refProjectId] - Per-referrer cross-chain HWM (unified across bridge AND burn):
bridgedOutOf[chainId][refProjectId] - Cumulative deposits received:
totalDeposited - Volume ledger: read-only, from
JBTerminalStoreinnana-core-v6(normalized to NATIVE 18-dec) - Vesting state: lives in
JBTokenDistributorinnana-distributor-v6oncepushTo/claimAndPushforwards - Sucker outboxes / inboxes: live in
nana-suckers-v6
High-Signal Tests
test/JBReferralSplitHook.t.sol— unit tests for every revert path and storage updatedeploy-all-v6/test/fork/ReferralRewardCrossChainFork.t.sol— 28 cross-chain E2E tests + 4 096-run fuzz on the pro-rata math, covering same-chain push, cross-chain bridge, cross-chain claim with different IDs, USDC fee flow, burn-on-strand, burn-unbridgeable, and shared-HWM invariants
Install
npm install @bananapus/referral-split-hook-v6Development
npm install
forge build --deny notes
forge test --deny notesUseful scripts:
npm run test:forknpm run deploy:mainnetsnpm run deploy:testnets
Repository Layout
src/
JBReferralSplitHook.sol
interfaces/
IJBReferralSplitHook.sol
test/
JBReferralSplitHook.t.sol
script/
Deploy.s.solRisks And Notes
- The hook trusts
JBTerminalStoreas a volume oracle. Any operational issue with the originating terminal's attribution flows through here. pushTo,bridgeRemote,claimAndPush, andburnUnbridgeableCreditForare all permissionless. Frontends and keepers are expected to call them regularly.- Burned credit is permanently irrecoverable for the would-be referrer. The policy chooses certainty over preserving every cent for an off-chain decision.
For AI Agents
- This repo is a thin router. Volume attribution lives upstream in
nana-core-v6; vesting + claim lives downstream innana-distributor-v6; bridge plumbing lives innana-suckers-v6. - If you find yourself adding accounting logic here, it likely belongs in one of those repos instead.
- The deferral-vs-stranding decision matrix in RISKS.md § 7 is the design contract. Every new entrypoint must match it.
