@bananapus/core-v6 0.0.28 → 0.0.29
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/ARCHITECTURE.md +3 -3
- package/CHANGE_LOG.md +26 -0
- package/README.md +2 -2
- package/RISKS.md +8 -3
- package/SKILLS.md +5 -4
- package/package.json +1 -1
- package/script/DeployPeriphery.s.sol +14 -13
- package/src/JBChainlinkV3PriceFeed.sol +4 -1
- package/src/JBChainlinkV3SequencerPriceFeed.sol +1 -1
- package/src/JBController.sol +6 -2
- package/src/JBDirectory.sol +7 -0
- package/src/JBMultiTerminal.sol +23 -10
- package/src/JBTerminalStore.sol +7 -4
- package/src/interfaces/IJBController.sol +6 -0
- package/test/AuditFixes.t.sol +808 -0
- package/test/TestFeeFreeCashOutBypass.sol +2 -2
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +4 -10
- package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +2 -12
- package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +2 -5
- package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +2 -5
package/ARCHITECTURE.md
CHANGED
|
@@ -22,8 +22,8 @@ src/
|
|
|
22
22
|
├── JBERC20.sol — Cloneable ERC20Votes+Permit token
|
|
23
23
|
├── JBFeelessAddresses.sol — Fee-exempt address registry
|
|
24
24
|
├── JBDeadline.sol — Approval hook requiring minimum delay
|
|
25
|
-
├── JBChainlinkV3PriceFeed.sol — Chainlink v3 price feed with staleness check
|
|
26
|
-
├── JBChainlinkV3SequencerPriceFeed.sol — L2 sequencer-aware price feed
|
|
25
|
+
├── JBChainlinkV3PriceFeed.sol — Chainlink v3 price feed with staleness + answeredInRound check
|
|
26
|
+
├── JBChainlinkV3SequencerPriceFeed.sol — L2 sequencer-aware price feed (answer != 0 = down)
|
|
27
27
|
├── abstract/
|
|
28
28
|
│ ├── JBPermissioned.sol — Base for permission-checked contracts
|
|
29
29
|
│ └── JBControlled.sol — Base for controller-gated contracts
|
|
@@ -136,7 +136,7 @@ Owner -> JBMultiTerminal.sendPayoutsOf()
|
|
|
136
136
|
| Data Hook (cashout) | `IJBRulesetDataHook.beforeCashOutRecordedWith` | JBTerminalStore |
|
|
137
137
|
| Pay Hook | `IJBPayHook.afterPayRecordedWith` | JBMultiTerminal |
|
|
138
138
|
| Cash Out Hook | `IJBCashOutHook.afterCashOutRecordedWith` | JBMultiTerminal |
|
|
139
|
-
| Split Hook | `IJBSplitHook.processSplitWith` | JBMultiTerminal, JBController |
|
|
139
|
+
| Split Hook | `IJBSplitHook.processSplitWith` | JBMultiTerminal, JBController (try-catch; reverts emit `SplitHookReverted`) |
|
|
140
140
|
| Approval Hook | `IJBRulesetApprovalHook.approvalStatusOf` | JBRulesets |
|
|
141
141
|
|
|
142
142
|
## Dependencies
|
package/CHANGE_LOG.md
CHANGED
|
@@ -52,6 +52,26 @@ Also in this release:
|
|
|
52
52
|
|
|
53
53
|
`migrateBalanceOf` now charges the standard 2.5% protocol fee when migrating to a non-feeless terminal, consistent with all other fund egress. This also settles any `_feeFreeSurplusOf` liability that would otherwise be lost on the new terminal. The fee is deducted from the migrated balance before transfer. Feeless terminals are exempt.
|
|
54
54
|
|
|
55
|
+
### 0.8 JBDirectory -- ADD_TERMINALS Permission on Implicit Addition
|
|
56
|
+
|
|
57
|
+
`setPrimaryTerminalOf` now requires the `ADD_TERMINALS` permission when the specified terminal is not already in the project's terminal list. Previously, `setPrimaryTerminalOf` would implicitly add the terminal to the project's terminal list without any permission check beyond `SET_PRIMARY_TERMINAL`. Now, if `isTerminalOf(projectId, terminal)` returns false, the caller (or the account they're acting on behalf of) must also hold the `ADD_TERMINALS` permission for the project. Terminals already in the list are unaffected.
|
|
58
|
+
|
|
59
|
+
### 0.9 JBController -- Split Hook Try-Catch
|
|
60
|
+
|
|
61
|
+
`_sendReservedTokensToSplitsOf` now wraps the `split.hook.processSplitWith(...)` call in a try-catch. If a split hook reverts, the tokens already transferred to the hook remain with the hook (they were transferred via `safeTransfer` before the `processSplitWith` call), and the controller emits a `SplitHookReverted(projectId, hook, reason)` event instead of propagating the revert. This prevents a single reverting split hook from blocking the entire reserved token distribution. A new event `SplitHookReverted` was added to `IJBController`.
|
|
62
|
+
|
|
63
|
+
### 0.10 JBChainlinkV3SequencerPriceFeed -- Sequencer Down Check
|
|
64
|
+
|
|
65
|
+
The sequencer status check changed from `answer == 1` to `answer != 0`. The Chainlink sequencer uptime feed returns `0` for "up" and `1` for "down", but may return other non-zero values in future feed versions. The new check treats any non-zero answer as sequencer-down, which is the safer default.
|
|
66
|
+
|
|
67
|
+
### 0.11 JBMultiTerminal -- forceApprove
|
|
68
|
+
|
|
69
|
+
`_beforeSendFor` now uses `forceApprove` (from OpenZeppelin's `SafeERC20`) instead of `safeIncreaseAllowance` when setting token allowances for outbound ERC-20 transfers. `safeIncreaseAllowance` could fail if a previous allowance was partially consumed and a non-standard token (e.g., USDT) requires the allowance to be zero before setting a new value. `forceApprove` resets the allowance to zero first if needed, then sets it to the desired amount.
|
|
70
|
+
|
|
71
|
+
### 0.12 JBChainlinkV3PriceFeed -- answeredInRound Check
|
|
72
|
+
|
|
73
|
+
`currentUnitPrice` now checks `answeredInRound < roundId` after retrieving `latestRoundData()`. If `answeredInRound` is less than `roundId`, the answer was carried over from a previous round and the current round is incomplete. This reverts with `JBChainlinkV3PriceFeed_IncompleteRound()`, complementing the existing `updatedAt == 0` check to catch additional incomplete round scenarios.
|
|
74
|
+
|
|
55
75
|
### 0.7 JBMultiTerminal -- Self-Pay Revert
|
|
56
76
|
|
|
57
77
|
`_pay` now reverts with `JBMultiTerminal_MintNotAllowed()` when `payer == address(this)`. This prevents same-project intra-terminal payout splits (where `preferAddToBalance == false`) from minting tokens against existing balance without new funds entering the system. The try-catch in `JBPayoutSplitGroupLib` catches this revert and restores the balance via `recordAddedBalanceFor`. Projects that want to mint should do so explicitly via the controller.
|
|
@@ -166,6 +186,7 @@ Parameters changed from `memory` to `calldata` for gas efficiency.
|
|
|
166
186
|
|----------|-------|
|
|
167
187
|
| `IJBTokens` | `SetTokenMetadata(uint256 indexed projectId, string name, string symbol, address caller)` |
|
|
168
188
|
| `IJBPermitTerminal` | `Permit2AllowanceFailed(address indexed token, address indexed owner, bytes reason)` |
|
|
189
|
+
| `IJBController` | `SplitHookReverted(uint256 indexed projectId, address hook, bytes reason)` |
|
|
169
190
|
|
|
170
191
|
### 2.3 New Errors
|
|
171
192
|
|
|
@@ -328,6 +349,7 @@ No changes.
|
|
|
328
349
|
| **Migration lifecycle** | `afterReceiveMigrationFrom` added — called by directory after migration completes (validates caller is directory). |
|
|
329
350
|
| **`launchRulesetsFor` permission** | Changed from `QUEUE_RULESETS` to `LAUNCH_RULESETS`. |
|
|
330
351
|
| **Split token transfer assertion** | `assert(allowance == 0)` replaced with explicit `revert JBController_TerminalTokensNotTransferred()`. |
|
|
352
|
+
| **Split hook try-catch** | `processSplitWith` in `_sendReservedTokensToSplitsOf` is now wrapped in a try-catch. A reverting split hook emits `SplitHookReverted` instead of propagating. Transferred tokens stay with the hook. |
|
|
331
353
|
| **Code organization** | External views moved after external transactions. Internal views moved to end of file. |
|
|
332
354
|
|
|
333
355
|
### 8.2 JBMultiTerminal
|
|
@@ -341,6 +363,7 @@ No changes.
|
|
|
341
363
|
| **Split payout documentation** | Failed split payouts documented as consuming payout limit by design. |
|
|
342
364
|
| **Fee-free cashout bypass prevention** | New `_feeFreeSurplusOf` mapping tracks cumulative fee-free intra-terminal payouts per project/token. Capped at remaining balance after outflows including non-zero-tax/feeless cashouts (non-fee-free funds leave first). During zero-tax cashouts, fees are charged up to this tracked amount (then decremented). Cleared on migration. See Section 0.2. |
|
|
343
365
|
| **beneficiaryIsFeeless passthrough** | `cashOutTokensOf` now passes `_isFeeless(beneficiary)` to `recordCashOutFor`, which forwards it to data hooks via `JBBeforeCashOutRecordedContext.beneficiaryIsFeeless`. |
|
|
366
|
+
| **forceApprove** | `_beforeSendFor` now uses `forceApprove` instead of `safeIncreaseAllowance` for outbound ERC-20 token approvals, avoiding reverts on tokens (e.g. USDT) that require zero allowance before re-approval. |
|
|
344
367
|
|
|
345
368
|
### 8.3 JBRulesets
|
|
346
369
|
|
|
@@ -356,6 +379,7 @@ No changes.
|
|
|
356
379
|
| Change | Description |
|
|
357
380
|
|--------|-------------|
|
|
358
381
|
| **Migration ordering** | `setControllerOf` now calls `migrate()` on the old controller BEFORE updating `controllerOf` in storage (so `migrate()` runs while the directory still points to the old controller). After updating storage, it calls `afterReceiveMigrationFrom` on the new controller. |
|
|
382
|
+
| **ADD_TERMINALS permission on implicit addition** | `setPrimaryTerminalOf` now requires the `ADD_TERMINALS` permission when the terminal is not already in the project's terminal list. Previously, implicit addition had no permission check beyond `SET_PRIMARY_TERMINAL`. |
|
|
359
383
|
|
|
360
384
|
### 8.5 JBSplits
|
|
361
385
|
|
|
@@ -380,6 +404,7 @@ No changes.
|
|
|
380
404
|
| Change | Description |
|
|
381
405
|
|--------|-------------|
|
|
382
406
|
| **Incomplete round check order** | The check for `updatedAt == 0` (incomplete round) now runs BEFORE the stale price check, avoiding false stale errors on incomplete rounds. |
|
|
407
|
+
| **answeredInRound check** | `currentUnitPrice` now checks `answeredInRound < roundId` and reverts with `IncompleteRound` if the answer was carried over from a previous round. |
|
|
383
408
|
|
|
384
409
|
### 8.9 JBChainlinkV3SequencerPriceFeed
|
|
385
410
|
|
|
@@ -387,6 +412,7 @@ No changes.
|
|
|
387
412
|
|--------|-------------|
|
|
388
413
|
| **Typo fix** | Error parameter `gradePeriodTime` corrected to `gracePeriodTime` in `JBChainlinkV3SequencerPriceFeed_SequencerDown`. |
|
|
389
414
|
| **Threshold docs** | Constructor parameter `threshold` documentation corrected from "blocks" to "seconds". |
|
|
415
|
+
| **Sequencer down check** | Changed `answer == 1` to `answer != 0` in the sequencer uptime check. Any non-zero answer is now treated as sequencer-down, which is the safer default for future Chainlink feed versions. |
|
|
390
416
|
|
|
391
417
|
### 8.10 Solidity Version
|
|
392
418
|
|
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ graph TD;
|
|
|
147
147
|
| Contract | Description |
|
|
148
148
|
|----------|-------------|
|
|
149
149
|
| `JBERC20` | Cloneable ERC-20 with ERC20Votes and ERC20Permit. Deployed by `JBTokens` via `Clones.clone()`. Owned by `JBTokens`. Name and symbol can be updated by the project owner via `JBController.setTokenMetadataOf`. |
|
|
150
|
-
| `JBChainlinkV3PriceFeed` | `IJBPriceFeed` backed by a Chainlink `AggregatorV3Interface` with staleness threshold. Rejects negative/zero prices and
|
|
150
|
+
| `JBChainlinkV3PriceFeed` | `IJBPriceFeed` backed by a Chainlink `AggregatorV3Interface` with staleness threshold. Rejects negative/zero prices, incomplete rounds (`updatedAt == 0`), and stale answers carried from previous rounds (`answeredInRound < roundId`). |
|
|
151
151
|
| `JBChainlinkV3SequencerPriceFeed` | Extends `JBChainlinkV3PriceFeed` with L2 sequencer uptime validation and grace period for Optimism/Arbitrum. |
|
|
152
152
|
| `JBMatchingPriceFeed` | Returns 1:1 price (e.g., ETH/NATIVE_TOKEN on applicable chains). Lives in `src/periphery/`. |
|
|
153
153
|
|
|
@@ -196,7 +196,7 @@ This section summarizes known risks and design trade-offs. None of these are unm
|
|
|
196
196
|
|
|
197
197
|
### Reentrancy
|
|
198
198
|
|
|
199
|
-
The protocol does not use an explicit `ReentrancyGuard`. Instead, it relies on **state ordering**: all storage writes (balance updates, token mints/burns, payout limit usage) are completed before any external calls (hooks, split payouts, fee processing). This means re-entering a function sees already-updated state, preventing double-spends and double-payouts. The `try-catch` pattern around external calls ensures that hook failures do not leave the protocol in an inconsistent state -- failed calls return funds to the project balance.
|
|
199
|
+
The protocol does not use an explicit `ReentrancyGuard`. Instead, it relies on **state ordering**: all storage writes (balance updates, token mints/burns, payout limit usage) are completed before any external calls (hooks, split payouts, fee processing). This means re-entering a function sees already-updated state, preventing double-spends and double-payouts. The `try-catch` pattern around external calls ensures that hook failures do not leave the protocol in an inconsistent state -- failed calls return funds to the project balance. This includes reserved token split hooks: `processSplitWith` is wrapped in try-catch, and a reverting hook emits `SplitHookReverted` instead of blocking distribution.
|
|
200
200
|
|
|
201
201
|
### Unbounded Arrays
|
|
202
202
|
|
package/RISKS.md
CHANGED
|
@@ -50,7 +50,7 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
|
|
|
50
50
|
| `_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 |
|
|
51
51
|
| `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 |
|
|
52
52
|
| `processHeldFeesOf` | `delete _heldFeesOf[...][currentIndex]`, `_nextHeldFeeIndexOf` incremented | `_processFee` -> `this.executeProcessFee` -> `terminal.pay` | LOW -- index advanced before external call; re-reads from storage each iteration |
|
|
53
|
-
| `_sendReservedTokensToSplitsOf` | `pendingReservedTokenBalanceOf` zeroed, tokens minted to controller | Split hooks, terminal payments | LOW -- pending balance cleared before minting prevents double-distribution |
|
|
53
|
+
| `_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. |
|
|
54
54
|
| `_useAllowanceOf` | `STORE.recordUsedAllowanceOf` (allowance consumed, balance decremented) | `_takeFeeFrom` (fee payment/holding), `_transferFrom` (beneficiary) | LOW -- allowance consumed before calls |
|
|
55
55
|
| `migrateBalanceOf` | `STORE.recordTerminalMigration` (balance zeroed), `_takeFeeFrom` (if non-feeless destination) | `to.addToBalanceOf` | LOW -- balance zeroed before transfer, fee deducted before transfer |
|
|
56
56
|
|
|
@@ -59,6 +59,7 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
|
|
|
59
59
|
- **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.
|
|
60
60
|
- **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.
|
|
61
61
|
- **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.
|
|
62
|
+
- **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).
|
|
62
63
|
- **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
64
|
|
|
64
65
|
### Key Backstop
|
|
@@ -74,6 +75,10 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
|
|
|
74
75
|
- **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.
|
|
75
76
|
- **`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.
|
|
76
77
|
|
|
78
|
+
### Directory Terminal Addition
|
|
79
|
+
|
|
80
|
+
- **`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.
|
|
81
|
+
|
|
77
82
|
### Migration
|
|
78
83
|
|
|
79
84
|
- **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.
|
|
@@ -100,8 +105,8 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
|
|
|
100
105
|
|
|
101
106
|
### Price Feed Reverts
|
|
102
107
|
|
|
103
|
-
- 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`.
|
|
104
|
-
- L2 sequencer downtime triggers `JBChainlinkV3SequencerPriceFeed` to revert during downtime + grace period.
|
|
108
|
+
- 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).
|
|
109
|
+
- 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.
|
|
105
110
|
- Single-currency projects (where `amount.currency == ruleset.baseCurrency()`) are unaffected.
|
|
106
111
|
- Price feeds are immutable once set in `JBPrices` -- a broken feed cannot be replaced.
|
|
107
112
|
|
package/SKILLS.md
CHANGED
|
@@ -21,8 +21,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
21
21
|
| `JBPrices` | Price feed registry with project-specific and protocol-wide default feeds. Immutable once set. |
|
|
22
22
|
| `JBERC20` | Cloneable ERC-20 with Votes + Permit. Owned by `JBTokens`. Deployed via `Clones.clone()`. |
|
|
23
23
|
| `JBFeelessAddresses` | Allowlist for fee-exempt addresses. |
|
|
24
|
-
| `JBChainlinkV3PriceFeed` | Chainlink AggregatorV3 price feed with staleness threshold. Rejects negative/zero prices. |
|
|
25
|
-
| `JBChainlinkV3SequencerPriceFeed` | L2 sequencer-aware Chainlink feed (Optimism/Arbitrum) with grace period after restart. |
|
|
24
|
+
| `JBChainlinkV3PriceFeed` | Chainlink AggregatorV3 price feed with staleness threshold. Rejects negative/zero prices, incomplete rounds (`updatedAt == 0`), and stale answers carried from previous rounds (`answeredInRound < roundId`). |
|
|
25
|
+
| `JBChainlinkV3SequencerPriceFeed` | L2 sequencer-aware Chainlink feed (Optimism/Arbitrum) with grace period after restart. Treats any non-zero sequencer answer as down (`answer != 0`). |
|
|
26
26
|
| `JBDeadline` | Approval hook: rejects rulesets queued within `DURATION` seconds of start. Ships as `JBDeadline3Hours`, `JBDeadline1Day`, `JBDeadline3Days`, `JBDeadline7Days`. |
|
|
27
27
|
| `JBMatchingPriceFeed` | Always returns 1:1. For equivalent currencies (e.g. ETH/NATIVE_TOKEN). |
|
|
28
28
|
|
|
@@ -121,7 +121,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
121
121
|
| `isTerminalOf(uint256 projectId, IJBTerminal terminal)` | Checks if a terminal belongs to a project. |
|
|
122
122
|
| `setControllerOf(uint256 projectId, IERC165 controller)` | Sets the project's controller. |
|
|
123
123
|
| `setTerminalsOf(uint256 projectId, IJBTerminal[] terminals)` | Sets the project's terminals. |
|
|
124
|
-
| `setPrimaryTerminalOf(uint256 projectId, address token, IJBTerminal terminal)` | Sets the primary terminal for a token. |
|
|
124
|
+
| `setPrimaryTerminalOf(uint256 projectId, address token, IJBTerminal terminal)` | Sets the primary terminal for a token. Requires `ADD_TERMINALS` permission if the terminal is not already in the project's terminal list (implicit addition). |
|
|
125
125
|
| `setIsAllowedToSetFirstController(address addr, bool flag)` | Allows/disallows an address to set a project's first controller. Owner-only. |
|
|
126
126
|
|
|
127
127
|
### JBPrices
|
|
@@ -288,7 +288,7 @@ Quick-reference for the most common `JBPermissionIds` values (from `@bananapus/p
|
|
|
288
288
|
| `13` | `TRANSFER_CREDITS` | `JBController.transferCreditsFrom` |
|
|
289
289
|
| `14` | `SET_CONTROLLER` | `JBDirectory.setControllerOf` |
|
|
290
290
|
| `15` | `SET_TERMINALS` | `JBDirectory.setTerminalsOf` (can remove primary terminal) |
|
|
291
|
-
| `16` | `SET_PRIMARY_TERMINAL` | `JBDirectory.setPrimaryTerminalOf` |
|
|
291
|
+
| `16` | `SET_PRIMARY_TERMINAL` | `JBDirectory.setPrimaryTerminalOf` (also requires `ADD_TERMINALS` if the terminal is not already in the project's list) |
|
|
292
292
|
| `17` | `USE_ALLOWANCE` | `JBMultiTerminal.useAllowanceOf` |
|
|
293
293
|
| `18` | `SET_SPLIT_GROUPS` | `JBController.setSplitGroupsOf` |
|
|
294
294
|
| `19` | `ADD_PRICE_FEED` | `JBController.addPriceFeedFor` |
|
|
@@ -359,6 +359,7 @@ The most important events for indexing and off-chain monitoring. Indexed params
|
|
|
359
359
|
| `BurnTokens` | `IJBController` | `holder*`, `projectId*`, `tokenCount` |
|
|
360
360
|
| `SendReservedTokensToSplits` | `IJBController` | `rulesetId*`, `rulesetCycleNumber*`, `projectId*`, `owner`, `tokenCount`, `leftoverAmount` |
|
|
361
361
|
| `SendReservedTokensToSplit` | `IJBController` | `projectId*`, `rulesetId*`, `groupId*`, `split`, `tokenCount` |
|
|
362
|
+
| `SplitHookReverted` | `IJBController` | `projectId*`, `hook`, `reason` |
|
|
362
363
|
| `LaunchProject` | `IJBController` | `rulesetId`, `projectId`, `projectUri` |
|
|
363
364
|
| `QueueRulesets` | `IJBController` | `rulesetId`, `projectId` |
|
|
364
365
|
| `DeployERC20` | `IJBController` | `projectId*`, `deployer*`, `salt`, `saltHash`, `caller` |
|
package/package.json
CHANGED
|
@@ -147,31 +147,34 @@ contract DeployPeriphery is Script, Sphinx {
|
|
|
147
147
|
}
|
|
148
148
|
require(address(feed) != address(0), "Invalid price feed");
|
|
149
149
|
|
|
150
|
-
core.prices
|
|
150
|
+
try core.prices
|
|
151
151
|
.addPriceFeedFor({
|
|
152
152
|
projectId: 0,
|
|
153
153
|
pricingCurrency: JBCurrencyIds.USD,
|
|
154
154
|
unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
155
155
|
feed: feed
|
|
156
|
-
})
|
|
156
|
+
}) {}
|
|
157
|
+
catch {}
|
|
157
158
|
|
|
158
159
|
// WARN: We are using the same price feed as the native token for the USD price feed. Which is only valid on
|
|
159
160
|
// chains where Ether is the native asset. We *NEED* to update this when we deploy to a non-ether chain!
|
|
160
|
-
core.prices
|
|
161
|
+
try core.prices
|
|
161
162
|
.addPriceFeedFor({
|
|
162
163
|
projectId: 0, pricingCurrency: JBCurrencyIds.USD, unitCurrency: JBCurrencyIds.ETH, feed: feed
|
|
163
|
-
})
|
|
164
|
+
}) {}
|
|
165
|
+
catch {}
|
|
164
166
|
|
|
165
167
|
// If the native asset for this chain is ether, then the conversion from native asset to ether is 1:1.
|
|
166
168
|
// NOTE: We need to refactor this the moment we add a chain where its native token is *NOT* ether.
|
|
167
169
|
// As otherwise prices for the `NATIVE_TOKEN` will be incorrect!
|
|
168
|
-
core.prices
|
|
170
|
+
try core.prices
|
|
169
171
|
.addPriceFeedFor({
|
|
170
172
|
projectId: 0,
|
|
171
173
|
pricingCurrency: JBCurrencyIds.ETH,
|
|
172
174
|
unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
173
175
|
feed: matchingPriceFeed
|
|
174
|
-
})
|
|
176
|
+
}) {}
|
|
177
|
+
catch {}
|
|
175
178
|
|
|
176
179
|
// Deploy the USDC/USD price feed.
|
|
177
180
|
_deployUSDCFeed(L2GracePeriod);
|
|
@@ -279,14 +282,12 @@ contract DeployPeriphery is Script, Sphinx {
|
|
|
279
282
|
require(usdc.code.length > 0, "Invalid USDC address");
|
|
280
283
|
require(address(usdcFeed) != address(0), "Invalid USDC price feed");
|
|
281
284
|
|
|
282
|
-
|
|
285
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
286
|
+
try core.prices
|
|
283
287
|
.addPriceFeedFor({
|
|
284
|
-
projectId: 0,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
unitCurrency: uint32(uint160(usdc)),
|
|
288
|
-
feed: usdcFeed
|
|
289
|
-
});
|
|
288
|
+
projectId: 0, pricingCurrency: JBCurrencyIds.USD, unitCurrency: uint32(uint160(usdc)), feed: usdcFeed
|
|
289
|
+
}) {}
|
|
290
|
+
catch {}
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
/// @dev This helper predicts addresses using the Arachnid CREATE2 deployer, but actual deployments go through
|
|
@@ -50,12 +50,15 @@ contract JBChainlinkV3PriceFeed is IJBPriceFeed {
|
|
|
50
50
|
function currentUnitPrice(uint256 decimals) public view virtual override returns (uint256) {
|
|
51
51
|
// Get the latest round information from the feed.
|
|
52
52
|
// slither-disable-next-line unused-return
|
|
53
|
-
(, int256 price,, uint256 updatedAt,) = FEED.latestRoundData();
|
|
53
|
+
(uint80 roundId, int256 price,, uint256 updatedAt, uint80 answeredInRound) = FEED.latestRoundData();
|
|
54
54
|
|
|
55
55
|
// Make sure the round is finished (check before stale price to avoid false stale on incomplete rounds).
|
|
56
56
|
// slither-disable-next-line incorrect-equality
|
|
57
57
|
if (updatedAt == 0) revert JBChainlinkV3PriceFeed_IncompleteRound();
|
|
58
58
|
|
|
59
|
+
// Make sure the answer was provided in the current round.
|
|
60
|
+
if (answeredInRound < roundId) revert JBChainlinkV3PriceFeed_IncompleteRound();
|
|
61
|
+
|
|
59
62
|
// Make sure the price's update threshold is met.
|
|
60
63
|
if (block.timestamp > THRESHOLD + updatedAt) {
|
|
61
64
|
revert JBChainlinkV3PriceFeed_StalePrice(block.timestamp, THRESHOLD, updatedAt);
|
|
@@ -64,7 +64,7 @@ contract JBChainlinkV3SequencerPriceFeed is JBChainlinkV3PriceFeed {
|
|
|
64
64
|
if (startedAt == 0) revert JBChainlinkV3SequencerPriceFeed_InvalidRound();
|
|
65
65
|
|
|
66
66
|
// Revert if sequencer has too recently restarted or is currently down.
|
|
67
|
-
if (block.timestamp <= GRACE_PERIOD_TIME + startedAt || answer
|
|
67
|
+
if (block.timestamp <= GRACE_PERIOD_TIME + startedAt || answer != 0) {
|
|
68
68
|
revert JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting(
|
|
69
69
|
block.timestamp, GRACE_PERIOD_TIME, startedAt
|
|
70
70
|
);
|
package/src/JBController.sol
CHANGED
|
@@ -1024,7 +1024,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1024
1024
|
});
|
|
1025
1025
|
|
|
1026
1026
|
// slither-disable-next-line reentrancy-events
|
|
1027
|
-
split.hook
|
|
1027
|
+
try split.hook
|
|
1028
1028
|
.processSplitWith(
|
|
1029
1029
|
JBSplitHookContext({
|
|
1030
1030
|
token: address(token),
|
|
@@ -1034,7 +1034,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1034
1034
|
groupId: groupId,
|
|
1035
1035
|
split: split
|
|
1036
1036
|
})
|
|
1037
|
-
)
|
|
1037
|
+
) {}
|
|
1038
|
+
catch (bytes memory reason) {
|
|
1039
|
+
// If the hook reverts, the tokens already transferred to it stay with the hook.
|
|
1040
|
+
emit SplitHookReverted({projectId: projectId, hook: address(split.hook), reason: reason});
|
|
1041
|
+
}
|
|
1038
1042
|
// If the split has a project ID, try to pay the project. If that fails, pay the beneficiary.
|
|
1039
1043
|
} else {
|
|
1040
1044
|
// Pay the project using the split's beneficiary if one was provided. Otherwise, use the message
|
package/src/JBDirectory.sol
CHANGED
|
@@ -185,6 +185,13 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
|
|
|
185
185
|
revert JBDirectory_TokenNotAccepted(projectId, token, terminal);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// If the terminal is not already in the project's terminal list, require ADD_TERMINALS permission.
|
|
189
|
+
if (!isTerminalOf({projectId: projectId, terminal: terminal})) {
|
|
190
|
+
_requirePermissionFrom({
|
|
191
|
+
account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.ADD_TERMINALS
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
188
195
|
// Implicit terminal addition is by design. A primary terminal must be in the terminals list;
|
|
189
196
|
// implicit addition avoids requiring a separate addTerminalsOf call.
|
|
190
197
|
_addTerminalIfNeeded({projectId: projectId, terminal: terminal});
|
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -407,6 +407,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
407
407
|
beneficiary: beneficiary,
|
|
408
408
|
metadata: metadata
|
|
409
409
|
});
|
|
410
|
+
|
|
411
|
+
// Cap fee-free surplus at remaining balance.
|
|
412
|
+
// Why: _feeFreeSurplusOf was incremented by the full netPayoutAmount above, but if the
|
|
413
|
+
// destination project's data hook forwarded part of the payment to pay hooks, the store
|
|
414
|
+
// only recorded a partial balance increase. Without this cap, _feeFreeSurplusOf can exceed
|
|
415
|
+
// STORE.balanceOf, causing users to be overcharged fees on zero-tax cashouts.
|
|
416
|
+
// slither-disable-next-line reentrancy-eth
|
|
417
|
+
_capFeeFreeSurplus({projectId: split.projectId, token: token});
|
|
410
418
|
}
|
|
411
419
|
} else {
|
|
412
420
|
// If there's a beneficiary, send the funds directly to the beneficiary.
|
|
@@ -1075,7 +1083,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1075
1083
|
if (token == JBConstants.NATIVE_TOKEN) return amount;
|
|
1076
1084
|
|
|
1077
1085
|
// Otherwise, set the allowance, and the payValue should be 0.
|
|
1078
|
-
IERC20(token).
|
|
1086
|
+
IERC20(token).forceApprove({spender: to, value: amount});
|
|
1079
1087
|
return 0;
|
|
1080
1088
|
}
|
|
1081
1089
|
|
|
@@ -1141,8 +1149,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1141
1149
|
// Non-zero tax: fees apply to the full reclaim amount.
|
|
1142
1150
|
amountEligibleForFees += reclaimAmount;
|
|
1143
1151
|
reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: reclaimAmount, feePercent: FEE});
|
|
1144
|
-
// Cap fee-free surplus at remaining balance (non-fee-free funds leave first).
|
|
1145
|
-
_reduceFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
|
|
1146
1152
|
} else {
|
|
1147
1153
|
// Zero tax: fees apply only up to the fee-free surplus (round-trip prevention).
|
|
1148
1154
|
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
|
|
@@ -1153,9 +1159,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1153
1159
|
reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: feeableAmount, feePercent: FEE});
|
|
1154
1160
|
}
|
|
1155
1161
|
}
|
|
1156
|
-
} else {
|
|
1157
|
-
// Feeless beneficiary: fee logic skipped, but still cap fee-free surplus at remaining balance.
|
|
1158
|
-
_reduceFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
|
|
1159
1162
|
}
|
|
1160
1163
|
|
|
1161
1164
|
// Subtract the fee from the reclaim amount.
|
|
@@ -1182,6 +1185,16 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1182
1185
|
});
|
|
1183
1186
|
}
|
|
1184
1187
|
|
|
1188
|
+
// Cap fee-free surplus at remaining balance.
|
|
1189
|
+
// Why: this single call replaces per-branch calls so that EVERY cashout path (non-zero tax,
|
|
1190
|
+
// zero tax, and feeless beneficiary) gets the cap. Without it, the zero-tax path would use
|
|
1191
|
+
// a stale _feeFreeSurplusOf that may exceed STORE.balanceOf (e.g. if pay hooks reduced the
|
|
1192
|
+
// balance during a prior inbound payout), overcharging fees on round-trip prevention.
|
|
1193
|
+
// Placed after hook fulfillment so any further balance reductions from cashout hooks are
|
|
1194
|
+
// also accounted for.
|
|
1195
|
+
// slither-disable-next-line reentrancy-eth
|
|
1196
|
+
_capFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
|
|
1197
|
+
|
|
1185
1198
|
// Take the fee from all outbound reclaimings.
|
|
1186
1199
|
if (amountEligibleForFees != 0) {
|
|
1187
1200
|
_takeFeeFrom({
|
|
@@ -1702,7 +1715,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1702
1715
|
STORE.recordPayoutFor({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
1703
1716
|
|
|
1704
1717
|
// Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
|
|
1705
|
-
|
|
1718
|
+
_capFeeFreeSurplus({projectId: projectId, token: token});
|
|
1706
1719
|
|
|
1707
1720
|
// Get a reference to the project's owner.
|
|
1708
1721
|
// The owner will receive tokens minted by paying the platform fee and receive any leftover funds not sent to
|
|
@@ -1903,7 +1916,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1903
1916
|
STORE.recordUsedAllowanceOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
1904
1917
|
|
|
1905
1918
|
// Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
|
|
1906
|
-
|
|
1919
|
+
_capFeeFreeSurplus({projectId: projectId, token: token});
|
|
1907
1920
|
|
|
1908
1921
|
// Take a fee from the `amountPaidOut`, if needed.
|
|
1909
1922
|
// The net amount is the final amount withdrawn after the fee has been taken.
|
|
@@ -1945,11 +1958,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1945
1958
|
/// and then cashing out without incurring fees.
|
|
1946
1959
|
/// @param projectId The ID of the project.
|
|
1947
1960
|
/// @param token The token whose fee-free surplus to cap.
|
|
1948
|
-
function
|
|
1961
|
+
function _capFeeFreeSurplus(uint256 projectId, address token) internal {
|
|
1949
1962
|
// Get the current fee-free surplus for this project/token pair.
|
|
1950
1963
|
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][token];
|
|
1951
1964
|
|
|
1952
|
-
// Nothing to
|
|
1965
|
+
// Nothing to cap if there's no fee-free surplus tracked.
|
|
1953
1966
|
if (feeFreeSurplus == 0) return;
|
|
1954
1967
|
|
|
1955
1968
|
// Get the project's remaining balance (already decremented by the store's record call).
|
package/src/JBTerminalStore.sol
CHANGED
|
@@ -1223,10 +1223,13 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1223
1223
|
|
|
1224
1224
|
// Set the payout limit value to the amount still available to pay out during the ruleset.
|
|
1225
1225
|
{
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1226
|
+
// Saturating subtraction: if a new ruleset activates with a lower payout limit than
|
|
1227
|
+
// what was already used under the previous limit, `used` can exceed `amount`. Clamping
|
|
1228
|
+
// to zero prevents an underflow revert that would DOS cashouts and surplus views.
|
|
1229
|
+
uint256 used = usedPayoutLimitOf[
|
|
1230
|
+
terminal
|
|
1231
|
+
][projectId][accountingContext.token][ruleset.cycleNumber][payoutLimit.currency];
|
|
1232
|
+
uint256 remaining = payoutLimit.amount > used ? payoutLimit.amount - used : 0;
|
|
1230
1233
|
if (remaining > type(uint224).max) revert JBTerminalStore_Uint224Overflow(remaining);
|
|
1231
1234
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1232
1235
|
payoutLimit.amount = uint224(remaining);
|
|
@@ -102,6 +102,12 @@ interface IJBController is IERC165, IJBProjectUriRegistry, IJBDirectoryAccessCon
|
|
|
102
102
|
uint256 indexed projectId, JBSplit split, uint256 tokenCount, bytes reason, address caller
|
|
103
103
|
);
|
|
104
104
|
|
|
105
|
+
/// @notice A split hook's `processSplitWith` call reverted.
|
|
106
|
+
/// @param projectId The ID of the project.
|
|
107
|
+
/// @param hook The split hook that reverted.
|
|
108
|
+
/// @param reason The revert reason.
|
|
109
|
+
event SplitHookReverted(uint256 indexed projectId, address hook, bytes reason);
|
|
110
|
+
|
|
105
111
|
/// @notice Reserved tokens were sent to a specific split.
|
|
106
112
|
/// @param projectId The ID of the project.
|
|
107
113
|
/// @param rulesetId The ID of the ruleset during the distribution.
|