@bananapus/core-v6 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/RISKS.md CHANGED
@@ -1,109 +1,109 @@
1
1
  # Juicebox Core Risk Register
2
2
 
3
- This file focuses on the accounting, permission, and liveness risks inside the core protocol contracts that everything else in the V6 ecosystem composes with.
3
+ This file covers the main accounting, permission, and liveness risks in the core protocol contracts that the rest of V6 builds on.
4
4
 
5
- ## How to use this file
5
+ ## How To Use This File
6
6
 
7
- - Read `Priority risks` first; these are the core failures that would propagate far beyond this repo.
8
- - Use the detailed sections below for protocol accounting, reentrancy, access control, preview, and integration reasoning.
9
- - Treat `Invariants to Verify` as ecosystem-critical properties, not optional test ideas.
7
+ - Read `Priority risks` first. Those are the failures with the widest blast radius.
8
+ - Use the later sections when you need detail on accounting, reentrancy, access control, previews, or integrations.
9
+ - Treat `Invariants to verify` as core properties, not optional test ideas.
10
10
 
11
- ## Priority risks
11
+ ## Priority Risks
12
12
 
13
13
  | Priority | Risk | Why it matters | Primary controls |
14
14
  |----------|------|----------------|------------------|
15
- | P0 | Core accounting corruption | Terminal, store, and controller accounting are the source of truth for balances, surplus, fees, and supply. A bug here propagates everywhere. | Heavy invariant testing, previews aligned with settlement paths, and conservative external integrations. |
16
- | P0 | Permission or migration mistakes | Controllers, terminals, and operators can redirect authority or value if access control or migration sequencing is wrong. | Strict permission review, migration tests, and scrutiny of wildcard or root-like authority. |
17
- | P1 | Preview or settlement divergence | Many higher-level hooks and routers depend on previews matching reality closely enough to route safely. | Explicit preview analysis, regression tests, and downstream composition review. |
15
+ | P0 | Core accounting corruption | Terminal, store, and controller accounting define balances, surplus, fees, and supply for the whole ecosystem. | Invariant tests, preview/settlement alignment, and conservative integrations. |
16
+ | P0 | Permission or migration mistakes | Controllers, terminals, and operators can redirect authority or value if checks or sequencing are wrong. | Permission review, migration tests, and scrutiny of wildcard or root-like authority. |
17
+ | P1 | Preview or settlement drift | Hooks and routers often depend on previews being close to execution. | Preview analysis, regression tests, and downstream composition review. |
18
18
 
19
19
  ## 1. Trust Assumptions
20
20
 
21
- - **Hooks do not exploit reentrancy.** No `ReentrancyGuard` anywhere in core. All safety relies on checks-effects-interactions ordering and the `JBTerminalStore_InadequateTerminalStoreBalance` backstop. If a hook finds a code path where state is read before a prior write has settled, value extraction may be possible.
22
- - **Data hooks are honest.** A data hook has absolute control over payment weight, cash out tax rate, `effectiveTotalSupply`, `effectiveCashOutCount`, and fund-forwarding amounts. A malicious data hook can bypass the bonding curve entirely (e.g., set `effectiveTotalSupply = surplus` to get 1:1 redemptions) or divert 100% of incoming payments to external hooks. The protocol enforces `sum(hook.amount) <= payment.value` and `reclaimAmount + sum(hook.amount) <= project balance`, but within those bounds the hook is omnipotent. The terminal still burns the caller-supplied cash-out count; the hook-adjusted values affect pricing only.
23
- - **Price feeds do not lie.** Surplus calculations, currency conversions for payouts, and surplus allowance all depend on `JBPrices`. A manipulated or stale feed causes incorrect surplus values. Chainlink feeds have staleness thresholds and sequencer checks, but project-specific feeds registered via `allowAddPriceFeed` have no such guarantee -- a project owner can register a feed that returns any value.
24
- - **ERC-20 tokens behave standardly.** `_acceptFundsFor` uses a balance-before/after pattern, which handles fee-on-transfer tokens on ingress. However, outbound fee-on-transfer behavior and rebasing tokens that change balances between transactions will cause `balanceOf` in `JBTerminalStore` to diverge from actual terminal holdings. These accounting risks are limited to projects that opt into those accounting contexts. Missing-return-value tokens are handled by `SafeERC20`.
25
- - **Reentrant or abusive ERC-20s are out of scope.** Projects choose which accounting contexts this terminal will accept. A token that reenters `pay` or `addToBalanceOf` from `transferFrom`, or otherwise manipulates terminal balance observations mid-transfer, can break the assumptions behind `_acceptFundsFor`'s balance-delta accounting. Core does not harden against intentionally adversarial tokens here; projects that want safe accounting must only accept standard ERC-20s.
26
- - **Trusted forwarder is not compromised.** The ERC-2771 forwarder is immutable. If compromised, it can spoof `_msgSender()` for all permission-gated functions across `JBController`, `JBMultiTerminal`, `JBProjects`, `JBPrices`, and `JBPermissions`.
27
- - **Project #1 terminal remains functional.** If the fee beneficiary project's terminal reverts, `_processFee` catches the error and returns the fee amount to the originating project's balance via `_recordAddedBalanceFor`. This is an intentional fail-open design: the fee is forgiven and a `FeeReverted` event is emitted. For held fees specifically, `processHeldFeesOf` deletes the held-fee entry and advances the index *before* calling `_processFee`, so there is no retry path — once a held fee fails to process, it is permanently forgiven. This prevents a broken fee route from locking project funds indefinitely. The tradeoff is that protocol revenue (project #1) can be lost if the fee terminal is misconfigured. Core deployment alone does not fully initialize fee collection for project `#1`; fee-bearing flows should be treated as fail-open until the fee project's controller, terminals, and accounting contexts are configured.
28
- - **`OMNICHAIN_RULESET_OPERATOR` is trusted.** This immutable address bypasses owner permission checks for `launchRulesetsFor` and `queueRulesetsOf`. It can also indirectly set terminals through `launchRulesetsFor`, but cannot call `setTerminalsOf` directly. A compromised operator can queue arbitrary rulesets for any project. `DeployPeriphery` intentionally validates only that this address is nonzero, so correctness of the configured operator is an out-of-band deployment responsibility.
21
+ - **Hooks are not exploiting reentrancy.** Core does not use `ReentrancyGuard`. Safety depends on call ordering and the `JBTerminalStore_InadequateTerminalStoreBalance` backstop.
22
+ - **Data hooks are highly trusted.** A data hook can change payment weight, cash-out tax rate, `effectiveTotalSupply`, `effectiveCashOutCount`, and hook-forwarding amounts. The protocol only bounds the final amounts.
23
+ - **Price feeds are honest enough.** Surplus, payout conversions, and allowance math depend on `JBPrices`. Stale or manipulated feeds misprice the system.
24
+ - **Accepted ERC-20s behave like standard tokens.** Inbound fee-on-transfer handling is safer than outbound handling. Rebasing or nonstandard outbound behavior can still break accounting assumptions.
25
+ - **Accepted tokens are not actively adversarial.** Core does not harden against tokens that reenter or distort balance observations during transfer.
26
+ - **The trusted forwarder is not compromised.** If it is, `_msgSender()` can be spoofed across permission-gated contracts.
27
+ - **Project `#1` fee routing stays live enough.** If fee processing into project `#1` fails, core favors liveness and returns value to the originating project instead of trapping it. That can forgive fees.
28
+ - **`OMNICHAIN_RULESET_OPERATOR` is trusted.** This address can bypass some owner checks for ruleset flows and is a broad trust point.
29
29
 
30
30
  ## 2. Economic Risks
31
31
 
32
32
  ### Bonding Curve
33
33
 
34
- - **Zero cash out guard.** `cashOutFrom` returns 0 when `cashOutCount == 0` (early return). Auditors should verify no code path bypasses this guard or reaches the `cashOutCount >= totalSupply` branch with both values at 0.
35
- - **Pending reserved tokens inflate `totalSupply`.** `totalTokenSupplyWithReservedTokensOf()` adds `pendingReservedTokenBalanceOf` to `totalSupply`, reducing per-token cash out value. A project owner who delays calling `sendReservedTokensToSplitsOf()` can suppress cash out values. Auditors should model the magnitude of this effect for projects with large pending reserves.
36
- - **Externally managed token supply affects only that project's cash-out pricing.** If a project opts into `setTokenFor(...)`, `JBTokens.totalSupplyOf()` trusts the attached token's `totalSupply()`. Any minting, burning, rebasing, or other supply changes on that external token affect only that project's supply-sensitive pricing and cash-out math. Projects that want protocol-controlled supply should use `deployERC20For(...)` instead.
37
- - **`mulDiv` rounding.** The bonding curve's subadditivity property (`cashOutFrom(a) + cashOutFrom(b) <= cashOutFrom(a+b)`) can be violated by <0.01% due to floor rounding. Economically insignificant per operation but could accumulate across many small cash outs.
38
- - **Binary search in `minCashOutCountFor`.** The inverse cash out function uses binary search over `[1, totalSupply]`. For large supplies (>2^128), this is ~128 iterations of `mulDiv` calls. Verify gas cost remains bounded.
34
+ - **Zero cash-out guard.** `cashOutFrom` returns `0` when `cashOutCount == 0`. Verify no path bypasses that guard.
35
+ - **Pending reserved tokens lower cash-out value.** `totalTokenSupplyWithReservedTokensOf()` includes `pendingReservedTokenBalanceOf`, which can reduce per-token reclaim value until reserves are distributed.
36
+ - **External token supply only affects that project.** If a project uses `setTokenFor(...)`, the external token's `totalSupply()` feeds that project's cash-out math.
37
+ - **`mulDiv` rounding exists.** Split cash outs can differ slightly from a combined cash out because of floor rounding.
38
+ - **`minCashOutCountFor` uses binary search.** Large supplies increase loop count. Gas should stay bounded.
39
39
 
40
40
  ### Fee Arithmetic
41
41
 
42
- - **Forward vs. backward fee asymmetry.** `feeAmountFrom` (forward) uses `mulDiv(amount, 25, 1000)`. `feeAmountResultingIn` (backward) uses `mulDiv(amount, 1000, 975) - amount`. These are algebraically equivalent but rounding differs. In `_returnHeldFees`, both are used on the same held fee entry (forward to compute `feeAmount`, backward when partially returning). Verify the interplay never undercharges.
43
- - **Held fee amount mutation.** `_returnHeldFees` mutates `heldFee.amount` in-place via unchecked subtraction. If the accounting is off by even 1 wei in the wrong direction, this underflows and corrupts the held fee entry.
42
+ - **Forward and backward fee math round differently.** `feeAmountFrom` and `feeAmountResultingIn` are close but not identical under rounding. Their interaction matters in held-fee paths.
43
+ - **Held fee entries are mutated in place.** If the accounting is off by even one unit in the wrong direction, `_returnHeldFees` can corrupt the entry.
44
44
 
45
45
  ### Weight Decay
46
46
 
47
- - **Weight cache starvation as DoS.** Projects with short duration and nonzero `weightCutPercent` that run >20,000 cycles without a cache update will revert on `currentOf()` with `WeightCacheRequired`. This blocks all operations (pay, cash out, payouts). Anyone can call `updateRulesetWeightCache()` to fix it, but an attacker could create projects designed to hit this.
48
- - **Weight-cache correctness matters more than overflow.** Rulesets reject weights above `type(uint112).max` at queue time, and weight decay only reduces weight over time. The real risk surface is stale or missing cache progress causing `WeightCacheRequired` reverts or incorrect long-horizon simulations if cache updates are applied to the wrong base ruleset.
47
+ - **Stale weight cache can block a project.** Short-duration rulesets with nonzero `weightCutPercent` can hit `WeightCacheRequired` after enough cycles.
48
+ - **Weight-cache correctness matters more than overflow.** Overflow is already bounded at queue time. The real risk is stale or wrongly-updated cache state.
49
49
 
50
50
  ### Surplus Manipulation
51
51
 
52
- - **Cross-terminal surplus aggregation.** When `useTotalSurplusForCashOuts` is enabled, `recordCashOutFor` aggregates surplus across all terminals via `JBSurplus.currentSurplusOf()`, which calls `terminal.currentSurplusOf()` on each. This is an explicit trust boundary, not a local-accounting guarantee: projects should only enable it when every listed terminal is mutually trusted to report economically compatible surplus. Defense: `InadequateTerminalStoreBalance` still prevents extracting more than the paying terminal's actual local balance, but partial burns can reclaim more from that terminal than a purely local-surplus model would allow. When `useTotalSurplusForCashOuts` is false, only the single token being reclaimed contributes to the surplus calculation.
53
- - **Price feed inconsistency across terminals.** Different tokens in different terminals are converted to a common currency via `JBPrices`. If price feeds between terminals are stale or inconsistent, aggregated surplus can be inflated/deflated, affecting cash out reclaim amounts.
52
+ - **Cross-terminal surplus is a trust boundary.** When `useTotalSurplusForCashOuts` is enabled, one terminal can price a cash out using value reported by other terminals.
53
+ - **Cross-terminal price-feed mismatch changes reclaim values.** If feeds differ or go stale across terminals, aggregated surplus can be wrong.
54
54
 
55
55
  ## 3. Reentrancy Surface
56
56
 
57
- No `ReentrancyGuard` is used. The system relies on state ordering and the `InadequateTerminalStoreBalance` backstop.
57
+ Core does not use `ReentrancyGuard`. It relies on state ordering plus `InadequateTerminalStoreBalance` as the last balance-extraction backstop.
58
58
 
59
59
  ### External Call Map
60
60
 
61
61
  | Function | State Changes Before External Call | External Calls | Risk |
62
62
  |----------|-----------------------------------|----------------|------|
63
- | `_pay` | `STORE.recordPaymentFrom` (balance incremented), `controller.mintTokensOf` (tokens minted) | Pay hooks via `_fulfillPayHookSpecificationsFor` | LOW -- full state settlement before hooks |
64
- | `_cashOutTokensOf` | `STORE.recordCashOutFor` (balance decremented), `controller.burnTokensOf` (tokens burned), `_transferFrom` (beneficiary paid) | Cash out hooks via `_fulfillCashOutHookSpecificationsFor`, then `_takeFeeFrom` | MEDIUM -- beneficiary receives funds before hooks execute; hooks run before fees are taken |
65
- | `executePayout` | `STORE.recordPayoutFor` already consumed payout limit | `split.hook.processSplitWith`, `terminal.pay/addToBalance` | MEDIUM -- split hook receives funds and can re-enter; payout limit already consumed prevents double-payout |
66
- | `processHeldFeesOf` | `delete _heldFeesOf[...][currentIndex]`, `_nextHeldFeeIndexOf` incremented | `_processFee` -> `this.executeProcessFee` -> `terminal.pay` | LOW -- index advanced before external call; re-reads from storage each iteration |
67
- | `_sendReservedTokensToSplitsOf` | `pendingReservedTokenBalanceOf` zeroed, tokens minted to controller | Split hooks (try-catch), terminal payments | LOW -- pending balance cleared before minting prevents double-distribution. Split hook `processSplitWith` is wrapped in try-catch; a reverting hook emits `SplitHookReverted` but does not block distribution. Tokens already transferred to the hook via `safeTransfer` remain with the hook. |
68
- | `_useAllowanceOf` | `STORE.recordUsedAllowanceOf` (allowance consumed, balance decremented) | `_takeFeeFrom` (fee payment/holding), `_transferFrom` (beneficiary) | LOW -- allowance consumed before calls |
69
- | `migrateBalanceOf` | `STORE.recordTerminalMigration` (balance zeroed), `_takeFeeFrom` (if non-feeless destination) | `to.addToBalanceOf` | LOW -- balance zeroed before transfer, fee deducted before transfer |
70
-
71
- ### Cross-Function Reentrancy to Explore
72
-
73
- - **Pay hook -> `cashOutTokensOf`**: After `_pay` mints tokens, a pay hook could call `cashOutTokensOf`. The cash out sees post-payment balance and post-mint supply. Not profitable after fees in tested scenarios, but verify with data hooks that modify weights.
74
- - **Cash out hook -> `pay`**: During `_cashOutTokensOf`, after tokens are burned and beneficiary is paid, a cash out hook could call `pay()` adding to the balance. Fees haven't been taken yet at this point. Verify the fee calculation on `amountEligibleForFees` isn't affected.
75
- - **Split hook -> `pay` on same project**: During `sendPayoutsOf`, a split hook receives funds and calls `pay()` on the same project. Payout limit is consumed, but the payment increases balance and mints tokens. The funds came from the project's own balance, so no value creation -- but verify the accounting. Note: `_pay` now reverts with `MintNotAllowed` when `payer == address(this)` to prevent same-project intra-terminal payout splits from minting tokens against existing balance. The try-catch in the split group lib catches this and restores the balance.
76
- - **Reserved token split hook -> re-entry**: During `_sendReservedTokensToSplitsOf`, a split hook's `processSplitWith` is now wrapped in try-catch. A reverting hook emits `SplitHookReverted` and does not block distribution. Tokens transferred to the hook before the call remain with it. A hook that re-enters during `processSplitWith` sees post-mint state (pending reserved balance already zeroed).
77
- - **Fee processing -> any re-entry**: `_processFee` uses `this.executeProcessFee` (external call via try-catch). Inside, it calls `terminal.pay()` on project #1. If project #1 has a pay hook that calls back, the fee amount is already deducted.
63
+ | `_pay` | `STORE.recordPaymentFrom`, `controller.mintTokensOf` | Pay hooks | LOW |
64
+ | `_cashOutTokensOf` | `STORE.recordCashOutFor`, `controller.burnTokensOf`, beneficiary transfer | Cash-out hooks, then fee processing | MEDIUM |
65
+ | `executePayout` | `STORE.recordPayoutFor` already consumed payout limit | Split hooks, terminal pay/addToBalance | MEDIUM |
66
+ | `processHeldFeesOf` | Held-fee entry deleted and index advanced | `_processFee` -> `this.executeProcessFee` -> `terminal.pay` | LOW |
67
+ | `_sendReservedTokensToSplitsOf` | Pending reserved balance zeroed, tokens minted | Split hooks, terminal payments | LOW |
68
+ | `_useAllowanceOf` | `STORE.recordUsedAllowanceOf` | Fee processing, beneficiary transfer | LOW |
69
+ | `migrateBalanceOf` | `STORE.recordTerminalMigration` | `to.addToBalanceOf` | LOW |
70
+
71
+ ### Cross-Function Reentrancy To Explore
72
+
73
+ - **Pay hook -> `cashOutTokensOf`.** The hook sees post-payment balance and post-mint supply.
74
+ - **Cash-out hook -> `pay`.** The hook runs after burn and payout but before fee processing completes.
75
+ - **Split hook -> `pay` on the same project.** Core now reverts same-project intra-terminal self-pay minting, but the path is still worth checking.
76
+ - **Reserved-token split hook reentry.** Hooks see post-mint state after pending reserved balance is zeroed.
77
+ - **Fee processing reentry.** `_processFee` makes an external fee payment into project `#1`; hook behavior there still matters.
78
78
 
79
79
  ### Key Backstop
80
80
 
81
- `JBTerminalStore_InadequateTerminalStoreBalance` revert prevents extracting more than the recorded balance from any terminal regardless of reentrancy state. This is the final defense for all value extraction paths. Auditors should verify this check cannot be bypassed by manipulating the recorded balance (e.g., via `recordAddedBalanceFor`, which has no access control -- balance is keyed by `msg.sender`, so only a terminal can inflate its own balance).
81
+ `JBTerminalStore_InadequateTerminalStoreBalance` should stop any path from pulling more than the terminal's recorded balance. Auditors should verify no caller can inflate that recorded balance without the terminal actually holding the funds.
82
82
 
83
83
  ## 4. Access Control
84
84
 
85
85
  ### Permission System
86
86
 
87
- - **ROOT (ID 1) grants all permissions.** Including permissions not yet defined. Future permission IDs automatically fall under ROOT.
88
- - **ROOT + wildcard (`projectId = 0`) is allowed for self-grants only.** An account can grant its own operator ROOT on the wildcard project, giving that operator god-mode across all of the account's projects. This is powerful but legitimate — the account owner is explicitly choosing to delegate full control. However, a third-party caller who holds ROOT for a specific project **cannot** grant ROOT to others or set any permissions on the wildcard project on someone else's behalf. This prevents ROOT operators from escalating their own privileges beyond what the account owner originally granted. Auditors should verify that no indirect path allows a ROOT operator on project X to escalate to ROOT on project Y without the account owner's direct action.
89
- - **Empty permission arrays pass `hasPermissions`.** By design (vacuous truth). Any caller that expects "the operator has at least one of these permissions" must validate the array is non-empty.
90
- - **`OMNICHAIN_RULESET_OPERATOR` bypass.** This immutable address can `launchRulesetsFor`, `queueRulesetsOf`, and set terminals for any project without owner permission. The trust assumption is that this operator only queues rulesets that the omnichain deployer's logic permits. If this address is an EOA or an upgradeable contract, it is a single point of failure for all projects.
87
+ - **ROOT grants all permissions.** That includes permissions added in the future.
88
+ - **ROOT plus wildcard is allowed only for self-grants.** An account can delegate broad power over its own projects, but third parties should not be able to escalate into it.
89
+ - **Empty permission arrays pass `hasPermissions`.** Callers must check for non-empty arrays if that matters to their logic.
90
+ - **`OMNICHAIN_RULESET_OPERATOR` is a broad bypass.** It can queue or launch rulesets for any project.
91
91
 
92
92
  ### Directory Terminal Addition
93
93
 
94
- - **`setPrimaryTerminalOf` implicit terminal addition** now requires the `ADD_TERMINALS` permission when the terminal is not already in the project's terminal list. This closes a gap where `SET_PRIMARY_TERMINAL` alone could silently add a new terminal without the `ADD_TERMINALS` permission check. If the terminal is already in the list, no additional permission is needed.
94
+ - **`setPrimaryTerminalOf` can also add a terminal.** When the terminal is not already installed, the call must satisfy `ADD_TERMINALS` as well as the primary-terminal permission.
95
95
 
96
96
  ### Migration
97
97
 
98
- - **Controller migration** requires `allowSetController` in the current ruleset. During migration, `JBController.migrate()` reverts if there are pending reserved tokens. An attacker cannot front-run migration to inflate pending reserves (they'd need mint permission), but a project with organic pending reserves must distribute them first.
99
- - **Terminal migration** requires `allowTerminalMigration` in the current ruleset. Held fees are intentionally NOT migrated -- they belong to project #1. Migration to a non-feeless terminal charges the standard 2.5% protocol fee on the full balance, settling any `_feeFreeSurplusOf` liability.
100
- - **Directory updates** (`setTerminalsOf`, `setControllerOf`) are gated by `IJBDirectoryAccessControl` checks that read from the current ruleset's metadata flags. If the current ruleset allows these changes, anyone with the appropriate permission can redirect all of a project's fund flows.
98
+ - **Controller migration depends on ruleset permission.** `allowSetController` must be active, and migration fails if reserved tokens are still pending.
99
+ - **Terminal migration also depends on ruleset permission.** Held fees are not migrated, and migration into a non-feeless terminal charges the normal protocol fee.
100
+ - **Directory updates are high-impact.** `setTerminalsOf` and `setControllerOf` can redirect a project's fund and authority flow.
101
101
 
102
102
  ### Ruleset Queuing
103
103
 
104
- - Only the project's controller can call `RULESETS.queueFor()` (enforced by `onlyControllerOf` modifier).
105
- - The controller allows queuing by the project owner, anyone with `QUEUE_RULESETS` permission, or the `OMNICHAIN_RULESET_OPERATOR`.
106
- - For `duration = 0` projects, a queued ruleset takes effect immediately. This means an owner can atomically change all project economics (weight, tax rate, splits, payout limits) in the same transaction as other operations.
104
+ - Only the current controller can call `RULESETS.queueFor()`.
105
+ - The controller lets the owner, an allowed operator, or `OMNICHAIN_RULESET_OPERATOR` queue rulesets.
106
+ - For `duration = 0` projects, a queued ruleset can take effect immediately.
107
107
 
108
108
  ## 5. DoS Vectors
109
109
 
@@ -111,137 +111,105 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
111
111
 
112
112
  | Array | Growth Mechanism | Cleanup | Risk |
113
113
  |-------|-----------------|---------|------|
114
- | `_heldFeesOf[projectId][token]` | Each held-fee payout appends | `_nextHeldFeeIndexOf` pointer skips processed; full delete when all processed | MODERATE -- if held fees accumulate faster than the 28-day unlock window, the array grows unboundedly. `processHeldFeesOf` takes a `count` param, so partial processing is possible. |
115
- | `splits[]` | Set by project owner per ruleset | Replaced wholesale | MODERATE -- no explicit cap. At 100+ splits, `_sendPayoutsToSplitGroupOf` gas exceeds 10M. Percentage constraint limits useful splits to ~300-500 but doesn't prevent a malicious owner from setting more. |
116
- | `_accountingContextsOf[projectId]` | `addAccountingContextsFor` (append-only) | Never shrinks | LOW -- duplicate prevention limits growth; realistic max ~100 tokens. But since it's append-only, a project that accepts many tokens over time cannot remove old ones. |
117
- | Payout limits / surplus allowances | Set per ruleset | Replaced per ruleset | LOW -- currency ordering constraint limits ~30-50. |
118
- | `_terminalsOf[projectId]` | `setTerminalsOf` (replaced wholesale) | Replaced | LOW -- realistic max 5-10. |
114
+ | `_heldFeesOf[projectId][token]` | Each held-fee payout appends | Index pointer skips processed entries | MODERATE |
115
+ | `splits[]` | Set by project owner per ruleset | Replaced wholesale | MODERATE |
116
+ | `_accountingContextsOf[projectId]` | `addAccountingContextsFor` append-only | Never shrinks | LOW |
117
+ | Payout limits / surplus allowances | Set per ruleset | Replaced per ruleset | LOW |
118
+ | `_terminalsOf[projectId]` | `setTerminalsOf` replace-only | Replaced | LOW |
119
119
 
120
120
  ### Price Feed Reverts
121
121
 
122
- - If a Chainlink feed is stale beyond its threshold, `JBChainlinkV3PriceFeed` reverts. This blocks all multi-currency operations for projects using that feed: `pay`, `cashOutTokensOf`, `sendPayoutsOf`, `useAllowanceOf`. The feed also reverts with `IncompleteRound` when `answeredInRound < roundId` (answer carried from a previous round).
123
- - L2 sequencer downtime triggers `JBChainlinkV3SequencerPriceFeed` to revert during downtime + grace period. The sequencer check uses `answer != 0` (any non-zero value = down), which is forward-compatible with future Chainlink feed versions that may use values other than `1` for the down state.
124
- - Single-currency projects (where `amount.currency == ruleset.baseCurrency()`) are unaffected.
125
- - Price feeds are immutable once set in `JBPrices` -- a broken feed cannot be replaced.
122
+ - Stale or incomplete Chainlink data can block multi-currency operations.
123
+ - L2 sequencer downtime can also block feeds behind a sequencer-check wrapper.
124
+ - Single-currency projects are unaffected when they do not need conversion.
125
+ - Price feeds are immutable once set in `JBPrices`.
126
126
 
127
127
  ### Approval Hook Griefing
128
128
 
129
- - A reverting approval hook is caught by try-catch and treated as `Failed`. This causes fallback to `basedOnId` chain.
130
- - A gas-consuming approval hook (e.g., infinite loop) can DoS `currentOf()` via gas exhaustion. The try-catch does not limit gas. This is accepted risk since the project owner chose their own approval hook, but it means a malicious approval hook can permanently freeze its project's operations.
131
- - Approval hook rejection at a ruleset boundary triggers complex fallback behavior: the protocol simulates cycling from the last approved ruleset. Verify this simulation always produces economically correct results, especially when multiple rulesets are queued and rejected in sequence.
129
+ - A reverting approval hook is caught and treated as failed approval.
130
+ - A gas-burning approval hook can still DoS `currentOf()` by exhausting gas.
131
+ - Repeated approval-hook rejection at a ruleset boundary can create complex fallback behavior that needs testing.
132
132
 
133
133
  ### Other DoS Surfaces
134
134
 
135
- - `sendPayoutsOf` is callable by anyone (unless `ownerMustSendPayouts` is set). A split recipient that always reverts will cause that split's payout to fail, but the try-catch returns the amount to the project balance. Payout limit is still consumed. The project owner must wait until the next cycle.
136
- - `addAccountingContextsFor` is gated by `allowAddAccountingContext` in the ruleset, but the contexts array is append-only and never shrinks. Over many rulesets, this could grow large enough to cause gas issues in functions that iterate over all contexts (e.g., `currentSurplusOf` when no explicit contexts are passed).
135
+ - Failed split payouts consume payout limit even when value is returned to project balance.
136
+ - `addAccountingContextsFor` is append-only, so projects that add many contexts over time can make some loops more expensive.
137
137
 
138
138
  ## 6. Preview Functions
139
139
 
140
- `JBMultiTerminal.previewPayFor`, `JBMultiTerminal.previewCashOutFrom`, and `JBController.previewMintOf` are `view` functions that simulate operations without modifying state. They compose the same computation paths as the real operations.
140
+ `JBMultiTerminal.previewPayFor`, `JBMultiTerminal.previewCashOutFrom`, and `JBController.previewMintOf` are read-only simulations of state-changing operations.
141
141
 
142
- - **Data hooks are called during previews.** `previewPayFor` and `previewCashOutFrom` invoke `beforePayRecordedWith` and `beforeCashOutRecordedWith` on data hooks. A reverting data hook causes the preview to revert. A gas-consuming hook can cause the preview to run out of gas.
143
- - **Store previews take an explicit terminal parameter.** `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` take an explicit `terminal` address for balance/surplus lookups. Callers must pass a registered terminal to get correct results. The terminal-level functions (`JBMultiTerminal.previewPayFor` / `previewCashOutFrom`) handle this automatically by passing `address(this)`.
144
- - **No state modification risk.** Preview functions cannot change balances, mint/burn tokens, or consume limits. They are safe to call from any context.
145
- - **Preview-execution divergence.** Preview functions and their corresponding real operations share computation logic but execute in different contexts. `previewPayFor` calls `STORE.previewPayFrom` which invokes the data hook's `beforePayRecordedWith` via `staticcall`. The real `pay` path invokes the same hook but state changes between preview and execution (other payments, cash outs, ruleset transitions) can change the data hook's response. Callers should treat preview results as estimates, not guarantees — especially for projects with stateful data hooks.
146
- - **Gas griefing via data hooks in previews.** Since `previewPayFor` and `previewCashOutFrom` invoke data hooks, a gas-expensive data hook can make preview calls prohibitively expensive. This affects frontends and indexers that rely on preview functions for quote display. Unlike the real operations (which have economic incentive to complete), preview calls have no built-in gas limit on the data hook invocation.
142
+ - **Previews call data hooks.** A reverting or gas-heavy hook can break previews.
143
+ - **Store previews require the correct terminal input.** Passing the wrong terminal gives the wrong answer.
144
+ - **Previews do not mutate state.** They cannot consume limits, move funds, or mint and burn tokens.
145
+ - **Preview and execution can still drift.** Shared logic helps, but state can change between calls and hooks can be stateful.
146
+ - **Some read-only surplus views are not hook-aware.** `currentReclaimableSurplusOf` and `currentTotalReclaimableSurplusOf` intentionally skip data hooks.
147
147
 
148
148
  ## 7. Integration Risks
149
149
 
150
150
  ### Non-Standard ERC-20s
151
151
 
152
- - **Fee-on-transfer tokens**: Handled by `_acceptFundsFor` using balance-before/after pattern. The actual received amount is used, not the passed `amount`. However, `_transferFrom` for outbound transfers uses the nominal amount. If the token charges fees on transfer-out, the terminal's actual balance decreases more than `balanceOf` in the store records. Over time, `terminal.balance(token) < sum(store.balanceOf(projectId, terminal, token))`, breaking the balance conservation invariant for the projects that choose to use that token.
153
- - **Reentrant transfer hooks**: `_acceptFundsFor` assumes the token transfer itself does not recursively create another accepted inflow before the outer balance delta is finalized. This is treated as an accepted integration risk rather than a core invariant. Projects should not register ERC-20s with ERC-777-style hooks, reentrant `transferFrom`, or other adversarial transfer behavior as terminal accounting contexts.
154
- - **Rebasing tokens**: Tokens that change balances (e.g., stETH, AMPL) will cause `JBTerminalStore.balanceOf` to diverge from actual terminal holdings. Positive rebases create untracked surplus; negative rebases can cause `InadequateTerminalStoreBalance` reverts on withdrawals. This risk is limited to projects that opt into those rebasing accounting contexts.
155
- - **Tokens with blocklists** (e.g., USDC, USDT): If a split beneficiary or cash out beneficiary is blocklisted, the transfer reverts. For split payouts, try-catch returns the amount to the project. For cash out beneficiaries, the entire `cashOutTokensOf` call reverts.
156
- - **Low-decimal tokens** (e.g., USDC with 6 decimals): Weight and token counts use 18 decimals internally. The fixed-point conversion in `recordPaymentFrom` uses `mulDiv(amount.value, weight, weightRatio)`. With large weight values and small decimal tokens, precision loss may be significant.
152
+ - **Fee-on-transfer tokens.** Inbound handling is safer than outbound handling. Outbound transfer fees can leave store accounting higher than real holdings.
153
+ - **Reentrant transfer hooks.** Core treats them as an accepted integration risk, not a hardened invariant.
154
+ - **Rebasing tokens.** Positive or negative rebases can desync terminal balances from store balances.
155
+ - **Blocklist tokens.** Beneficiary-specific transfer failures can revert user cash outs or return payout value to the project.
156
+ - **Low-decimal tokens.** Fixed-point conversions can lose meaningful precision.
157
157
 
158
158
  ### Permit2 Interactions
159
159
 
160
- - **Permit2 is only used for inbound transfers.** `_acceptFundsFor` tries direct ERC-20 `transferFrom` first (if allowance is sufficient), then falls back to Permit2. The Permit2 `permit` call is wrapped in try-catch -- failure emits an event but doesn't revert the payment.
161
- - **Outbound transfers never use Permit2.** All outbound `_transferFrom` calls pass `from: address(this)`, which takes the direct transfer path (`safeTransfer` for ERC-20s, `Address.sendValue` for native token) and returns before reaching the Permit2 fallback. The Permit2 fallback in `_transferFrom` only exists for the inbound case where `from` is the payer (`_msgSender()`).
162
- - The `uint160` cast in `JBMultiTerminal._acceptFundsFor` limits Permit2 transfers to `type(uint160).max`. Amounts above this revert with `OverflowAlert`.
160
+ - Permit2 is only used for inbound transfers.
161
+ - Outbound transfers never rely on Permit2.
162
+ - The `uint160` cast in `_acceptFundsFor` caps Permit2 transfer size.
163
163
 
164
164
  ### Cross-Terminal Surplus Aggregation
165
165
 
166
- - `JBSurplus.currentSurplusOf` calls `terminal.currentSurplusOf()` on each terminal. These are external view calls with no gas limit. A malicious or gas-expensive terminal can cause this aggregation to revert, blocking cash outs for any project that has `useTotalSurplusForCashOuts` enabled and uses that terminal. When `useTotalSurplusForCashOuts` is false, surplus is computed internally by the store for only the reclaimed token, avoiding cross-terminal external calls.
167
- - The surplus calculation converts each terminal's balance to a common currency via price feeds. Rounding accumulates across terminals. With N terminals and M tokens each, there are N*M price conversions, each with up to 1 wei of rounding error.
166
+ - `JBSurplus.currentSurplusOf` makes external view calls into each terminal with no gas cap.
167
+ - Aggregated surplus also compounds price-conversion rounding across terminals.
168
168
 
169
- ### `addToBalanceOf` with Arbitrary Metadata
169
+ ### `addToBalanceOf` Metadata
170
170
 
171
- - `addToBalanceOf` accepts arbitrary `metadata` which is not validated by the terminal or store. The metadata is passed through to `afterAddToBalanceRecordedWith` callbacks. If a project's data hook or terminal extension interprets this metadata, malformed metadata could cause unexpected behavior. The core protocol ignores metadata in `addToBalanceOf` — it only affects hook processing.
171
+ - `addToBalanceOf` accepts arbitrary metadata.
172
+ - Core ignores that metadata directly, but hooks may interpret it.
172
173
 
173
174
  ### `recordAddedBalanceFor` Access Control
174
175
 
175
- - `JBTerminalStore.recordAddedBalanceFor` has **no access control**. Any address can call it. The balance is keyed by `msg.sender` (the terminal address), so only a terminal can inflate its own recorded balance. This is safe as long as all terminals correctly track their actual holdings. A buggy or malicious terminal implementation could call `recordAddedBalanceFor` without actually receiving tokens, inflating the recorded balance above actual holdings.
176
+ - `JBTerminalStore.recordAddedBalanceFor` has no explicit access control.
177
+ - The balance key includes `msg.sender`, so only a terminal can inflate its own recorded balance.
178
+ - A buggy or malicious terminal can still lie about funds it received.
176
179
 
177
- ## 8. Accepted Behaviors
178
-
179
- ### 8.1 Cross-terminal surplus is an explicit trust boundary
180
-
181
- When a project enables `useTotalSurplusForCashOuts`, core intentionally stops treating a single terminal's local
182
- balance as the full economic truth and instead trusts every registered terminal's reported surplus. This means a
183
- project can get richer cash-out pricing from value held elsewhere, but it also means a bad or economically
184
- incompatible terminal can distort the aggregate. This is accepted because cross-terminal projects explicitly opt into
185
- shared treasury semantics; the alternative is forcing every terminal to behave as an isolated silo. Projects should
186
- only enable this mode when all participating terminals are mutually trusted and economically compatible.
187
-
188
- ### 8.2 Held-fee forgiveness on failed fee routing is fail-open by design
189
-
190
- Core intentionally prefers liveness over strict protocol fee collection. If project `#1` cannot accept a fee payment,
191
- `_processFee` returns the fee amount to the originating project's balance instead of locking funds. For held fees,
192
- `processHeldFeesOf` advances the queue before retrying the payment, so a failed held-fee processing attempt
193
- permanently forgives that fee. This is accepted because a broken fee route should not brick project treasury flows.
194
- The tradeoff is explicit revenue leakage for the fee beneficiary when the fee route is unavailable or incompletely
195
- wired.
180
+ ### Split And Owner-Payout Failure Semantics
196
181
 
197
- ### 8.3 Surplus allowance is ruleset-scoped, not implicit-cycle-scoped
182
+ - Failed split payouts still consume payout limit.
183
+ - Failed owner payouts also still consume payout limit.
184
+ - Reserved-token split hook reverts can strand tokens at the hook after transfer.
198
185
 
199
- `usedSurplusAllowanceOf` is keyed by terminal, project, token, ruleset, and currency rather than by an independently
200
- incrementing "cycle" counter. For projects whose rulesets roll forward implicitly without a new ruleset ID, allowance
201
- usage carries forward until a new ruleset actually takes effect. This is accepted because surplus allowance is meant
202
- to be tied to the active ruleset's economics, not to a synthetic cycle abstraction layered on top of an unchanged
203
- ruleset. Integrators that expect per-cycle resets should queue distinct rulesets instead of relying on implicit
204
- rollover.
205
-
206
- ### 8.4 Core deployment alone leaves fee routing fail-open until periphery wiring completes
207
-
208
- Core contracts are intentionally deployable before project `#1` is fully operational. Until the fee project's
209
- controller, terminals, and accounting contexts are wired, fee-bearing flows remain fail-open: fees are forgiven back
210
- to the originating project rather than trapped. This is accepted because deployment sequencing across repos is staged,
211
- and the protocol prioritizes keeping project flows live during rollout over enforcing fee collection before the fee
212
- beneficiary is ready.
213
-
214
- ## 9. Invariants to Verify
215
-
216
- These should hold at all times and are the most productive targets for formal verification or invariant testing:
186
+ ## 8. Accepted Behaviors
217
187
 
218
- ### Balance Conservation
219
- - `terminal.balance(token) >= sum(store.balanceOf(projectId, terminal, token))` for all projects sharing a terminal. Fee amounts held but not yet processed are included in the terminal's actual balance but not in any project's store balance. Violation indicates a bug in fee handling or reentrancy.
188
+ ### 8.1 Cross-terminal surplus is opt-in shared trust
220
189
 
221
- ### Fund Conservation
222
- - Total inflows to a project (payments + `addToBalance`) >= total outflows (payouts + cash outs + surplus allowance usage + fees). Rounding should favor the protocol (fees round up, reclaims round down).
190
+ When a project enables `useTotalSurplusForCashOuts`, it is choosing shared treasury semantics across terminals. That can improve pricing, but it also means each listed terminal is part of the trust boundary.
223
191
 
224
- ### Fee Monotonicity
225
- - Project #1 balance only increases over time (fees flow in, never out via protocol mechanics). Exception: project #1 itself can pay out or cash out.
192
+ ### 8.2 Failed fee routing is intentionally fail-open
226
193
 
227
- ### Token Supply Consistency
228
- - `TOKENS.totalSupplyOf(projectId) == creditSupply + erc20.totalSupply()` at all times.
229
- - `totalTokenSupplyWithReservedTokensOf(projectId) == TOKENS.totalSupplyOf(projectId) + pendingReservedTokenBalanceOf[projectId]`.
194
+ If project `#1` cannot accept a fee payment, core prefers liveness over strict fee collection. For held fees, a failed processing attempt can forgive the fee permanently.
230
195
 
231
- ### Payout Limit Enforcement
232
- - `usedPayoutLimitOf[terminal][projectId][token][cycleNumber][currency] <= payoutLimitOf(...)` after every `recordPayoutFor`. Verify this holds even when the same project pays out from multiple terminals in the same cycle.
196
+ ### 8.3 Surplus allowance is keyed by ruleset, not by an abstract cycle
233
197
 
234
- ### Surplus Allowance Enforcement
235
- - `usedSurplusAllowanceOf[terminal][projectId][token][rulesetId][currency] <= surplusAllowanceOf(...)` after every `recordUsedAllowanceOf`.
198
+ `usedSurplusAllowanceOf` is keyed by `ruleset.id`. If a ruleset auto-rolls without a new ID, allowance usage carries forward.
236
199
 
237
- ### Cash Out Bound
238
- - `reclaimAmount + sum(hookSpecification.amounts) <= balanceOf[terminal][projectId][token]` after every `recordCashOutFor`. This is the `InadequateTerminalStoreBalance` check. Verify it is never circumvented.
200
+ ### 8.4 Fee routing starts fail-open until the wider deployment is wired
239
201
 
240
- ### Ruleset Existence
241
- - After `launchProjectFor()`, `RULESETS.currentOf(projectId)` always returns a valid ruleset (non-zero `cycleNumber`). A project in a state where `currentOf` returns an empty ruleset cannot accept payments (`RulesetNotFound` revert), but verify this cannot happen accidentally.
202
+ Core can be deployed before project `#1` is fully ready. During that period, fee-bearing flows may forgive fees instead of trapping funds.
242
203
 
243
- ### No Flash-Loan Profit
244
- - `pay() + cashOutTokensOf()` in the same transaction should never be profitable after fees. The 2.5% fee should make single-block round-trips unprofitable. Verify this holds when data hooks modify weights or cash out parameters.
204
+ ## 9. Invariants To Verify
245
205
 
246
- ### Held Fee Integrity
247
- - `sum(heldFee.amount for active entries) + sum(processed fees) == total fees ever taken with shouldHoldFees=true`. Active entries are those from `_nextHeldFeeIndexOf` to end of array. Verify `_returnHeldFees`' in-place mutation of `heldFee.amount` preserves this invariant.
206
+ - **Balance conservation:** `terminal.balance(token) >= sum(store.balanceOf(projectId, terminal, token))` for projects sharing a terminal.
207
+ - **Fund conservation:** project inflows should cover project outflows plus fees, with rounding favoring the protocol.
208
+ - **Fee monotonicity:** project `#1` should only gain protocol fees through normal mechanics.
209
+ - **Token supply consistency:** protocol credit supply, ERC-20 supply, and pending reserved supply should reconcile.
210
+ - **Payout-limit enforcement:** `usedPayoutLimitOf(...)` must stay `<= payoutLimitOf(...)`.
211
+ - **Surplus-allowance enforcement:** `usedSurplusAllowanceOf(...)` must stay `<= surplusAllowanceOf(...)`.
212
+ - **Cash-out bound:** reclaim plus hook-forwarded amounts must not exceed recorded balance.
213
+ - **Ruleset existence:** after launch, `RULESETS.currentOf(projectId)` should not accidentally go empty.
214
+ - **No flash-loan profit:** `pay()` followed by `cashOutTokensOf()` in one transaction should not be profitable after fees.
215
+ - **Held-fee integrity:** active held-fee entries plus processed fees should equal all fees ever taken under held-fee mode.
package/SKILLS.md CHANGED
@@ -2,20 +2,24 @@
2
2
 
3
3
  ## Use This File For
4
4
 
5
- - Use this file when the task touches protocol core behavior: payments, cash-outs, terminals, controller actions, rulesets, splits, tokens, permissions, or price feeds.
6
- - Start here if you know the issue is in core, then open the specific contract that owns the state transition you are debugging.
5
+ - Use this file when the task touches core protocol behavior: payments, cash outs, terminals, controller actions, rulesets, splits, tokens, permissions, or price feeds.
6
+ - Start here when you know the issue is in core. Then narrow it to one state transition before reading more broadly.
7
7
 
8
8
  ## Read This Next
9
9
 
10
10
  | If you need... | Open this next |
11
11
  |---|---|
12
- | Repo overview and protocol framing | [`README.md`](./README.md) |
12
+ | Repo overview and protocol framing | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
13
13
  | Controller and project lifecycle behavior | [`src/JBController.sol`](./src/JBController.sol), [`src/JBProjects.sol`](./src/JBProjects.sol), [`src/JBTokens.sol`](./src/JBTokens.sol) |
14
14
  | Payment, cash-out, surplus, and fee accounting | [`src/JBMultiTerminal.sol`](./src/JBMultiTerminal.sol), [`src/JBTerminalStore.sol`](./src/JBTerminalStore.sol), [`src/JBFundAccessLimits.sol`](./src/JBFundAccessLimits.sol) |
15
15
  | Rulesets, permissions, directory, and prices | [`src/JBRulesets.sol`](./src/JBRulesets.sol), [`src/JBPermissions.sol`](./src/JBPermissions.sol), [`src/JBDirectory.sol`](./src/JBDirectory.sol), [`src/JBPrices.sol`](./src/JBPrices.sol) |
16
16
  | Shared math, metadata parsing, and constants | [`src/libraries/`](./src/libraries/), [`src/structs/`](./src/structs/), [`src/enums/`](./src/enums/) |
17
17
  | Periphery helpers and deployment | [`src/periphery/`](./src/periphery/), [`script/Deploy.s.sol`](./script/Deploy.s.sol), [`script/DeployPeriphery.s.sol`](./script/DeployPeriphery.s.sol) |
18
- | Invariants, fork tests, and security/economic regressions | [`test/formal/`](./test/formal/), [`test/fork/`](./test/fork/), [`test/audit/`](./test/audit/), [`test/helpers/`](./test/helpers/) |
18
+ | Payment and cash-out entrypoints | [`references/entrypoints.md`](./references/entrypoints.md) |
19
+ | Packed metadata, errors, events, and hook return shapes | [`references/types-errors-events.md`](./references/types-errors-events.md) |
20
+ | Payment and cash-out behavior in tests | [`test/TestPayBurnRedeemFlow.sol`](./test/TestPayBurnRedeemFlow.sol), [`test/TestCashOut.sol`](./test/TestCashOut.sol), [`test/TestMultiTerminalSurplus.sol`](./test/TestMultiTerminalSurplus.sol), [`test/TestTerminalPreviewParity.sol`](./test/TestTerminalPreviewParity.sol) |
21
+ | Permissions, rulesets, and invariants | [`test/TestPermissions.sol`](./test/TestPermissions.sol), [`test/PermissionEscalation.t.sol`](./test/PermissionEscalation.t.sol), [`test/TestRulesetQueueing.sol`](./test/TestRulesetQueueing.sol), [`test/ComprehensiveInvariant.t.sol`](./test/ComprehensiveInvariant.t.sol), [`test/PermissionsInvariant.t.sol`](./test/PermissionsInvariant.t.sol) |
22
+ | Economic and exploit coverage | [`test/EconomicSimulation.t.sol`](./test/EconomicSimulation.t.sol), [`test/CoreExploitTests.t.sol`](./test/CoreExploitTests.t.sol), [`test/FlashLoanAttacks.t.sol`](./test/FlashLoanAttacks.t.sol), [`test/WeirdTokenTests.t.sol`](./test/WeirdTokenTests.t.sol), [`test/AuditFixes.t.sol`](./test/AuditFixes.t.sol) |
19
23
 
20
24
  ## Repo Map
21
25
 
@@ -28,7 +32,7 @@
28
32
 
29
33
  ## Purpose
30
34
 
31
- The core Juicebox V6 protocol on EVM: a modular system for launching treasury-backed tokens with configurable rulesets that govern payments, payouts, cash outs, and token issuance.
35
+ This is the core Juicebox V6 protocol on EVM. It lets projects launch treasury-backed tokens with configurable rulesets for payments, payouts, cash outs, and token issuance.
32
36
 
33
37
  ## Reference Files
34
38
 
@@ -39,6 +43,13 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
39
43
 
40
44
  ## Working Rules
41
45
 
42
- - Open source before relying on any summary here. This skill is a router, not the ground truth.
43
- - For runtime bugs, start from the terminal/controller/store contract that owns the state transition.
44
- - For config or weird value-shape issues, open `references/types-errors-events.md` before changing structs or metadata packing.
46
+ - Open the source before relying on any summary here.
47
+ - For runtime bugs, start from the terminal, controller, or store contract that owns the state transition.
48
+ - `JBMultiTerminal` and `JBTerminalStore` should usually be read together.
49
+ - Payment and cash-out previews are part of the protocol surface. Keep them aligned with execution.
50
+ - Payout limits reset by ruleset cycle number. Surplus allowances are keyed by `ruleset.id`. They do not always reset together.
51
+ - Fee handling is subtle. Re-check held fees, fee-free surplus tracking, and feeless-address behavior before changing payout or cash-out logic.
52
+ - Fee-free surplus is a bounded anti-bypass mechanism, not a general exemption bucket.
53
+ - For config or metadata-shape issues, open `references/types-errors-events.md` before changing structs or packed metadata.
54
+ - If previews, accounting, or fee behavior change, verify the other two as well.
55
+ - If a bug looks cross-repo, prove it is not caused by a hook, router, or deployer before patching core.