@bananapus/core-v6 0.0.33 → 0.0.35

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.
Files changed (55) hide show
  1. package/ADMINISTRATION.md +75 -348
  2. package/ARCHITECTURE.md +86 -44
  3. package/AUDIT_INSTRUCTIONS.md +29 -42
  4. package/README.md +22 -3
  5. package/RISKS.md +45 -1
  6. package/SKILLS.md +16 -4
  7. package/USER_JOURNEYS.md +130 -30
  8. package/foundry.toml +2 -0
  9. package/package.json +2 -2
  10. package/references/entrypoints.md +1 -1
  11. package/script/Deploy.s.sol +2 -1
  12. package/src/JBERC20.sol +100 -30
  13. package/src/JBTerminalStore.sol +64 -23
  14. package/src/JBTokens.sol +1 -1
  15. package/src/abstract/JBPermissioned.sol +28 -0
  16. package/src/interfaces/IJBRulesetDataHook.sol +6 -1
  17. package/src/interfaces/IJBToken.sol +3 -3
  18. package/src/structs/JBAccountingContext.sol +0 -1
  19. package/src/structs/JBAfterCashOutRecordedContext.sol +0 -1
  20. package/src/structs/JBAfterPayRecordedContext.sol +0 -1
  21. package/src/structs/JBBeforeCashOutRecordedContext.sol +0 -1
  22. package/src/structs/JBBeforePayRecordedContext.sol +0 -1
  23. package/src/structs/JBCashOutHookSpecification.sol +0 -1
  24. package/src/structs/JBCurrencyAmount.sol +0 -1
  25. package/src/structs/JBFee.sol +0 -1
  26. package/src/structs/JBFundAccessLimitGroup.sol +0 -1
  27. package/src/structs/JBPayHookSpecification.sol +0 -1
  28. package/src/structs/JBPermissionsData.sol +0 -1
  29. package/src/structs/JBRuleset.sol +0 -1
  30. package/src/structs/JBRulesetConfig.sol +0 -1
  31. package/src/structs/JBRulesetMetadata.sol +0 -1
  32. package/src/structs/JBRulesetWeightCache.sol +0 -1
  33. package/src/structs/JBRulesetWithMetadata.sol +0 -1
  34. package/src/structs/JBSingleAllowance.sol +0 -1
  35. package/src/structs/JBSplit.sol +0 -1
  36. package/src/structs/JBSplitGroup.sol +0 -1
  37. package/src/structs/JBSplitHookContext.sol +0 -1
  38. package/src/structs/JBTerminalConfig.sol +0 -1
  39. package/src/structs/JBTokenAmount.sol +0 -1
  40. package/test/TestCashOutHooks.sol +12 -2
  41. package/test/TestDataHookFuzzing.sol +4 -4
  42. package/test/TestForwardedTokenConsumption.sol +7 -1
  43. package/test/TestJBERC20Inheritance.sol +3 -1
  44. package/test/TestTokenFlow.sol +2 -2
  45. package/test/audit/CashOutReenterPay.t.sol +5 -0
  46. package/test/audit/CodexHeldFeeRounding.t.sol +159 -0
  47. package/test/helpers/TestBaseWorkflow.sol +1 -1
  48. package/test/units/static/JBERC20/JBERC20Setup.sol +8 -3
  49. package/test/units/static/JBERC20/TestInitialize.sol +12 -13
  50. package/test/units/static/JBERC20/TestName.sol +1 -1
  51. package/test/units/static/JBERC20/TestNonces.sol +2 -1
  52. package/test/units/static/JBERC20/TestSymbol.sol +1 -1
  53. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +1 -1
  54. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +4 -4
  55. package/test/units/static/JBTokens/JBTokensSetup.sol +5 -1
package/USER_JOURNEYS.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # User Journeys
2
2
 
3
- ## Who This Repo Serves
3
+ ## Repo Purpose
4
+
5
+ This repo is the canonical Juicebox V6 runtime surface.
6
+ It owns project identity, rulesets, terminal execution, treasury accounting, permissions, price feeds, and migration
7
+ paths. Most other V6 repos wrap or extend this behavior rather than replacing it.
8
+
9
+ ## Primary Actors
4
10
 
5
11
  - founders launching and evolving Juicebox projects
6
12
  - supporters paying projects in the asset a terminal accepts
@@ -8,109 +14,203 @@
8
14
  - operators managing permissions, splits, fund access limits, and rulesets
9
15
  - integrators wiring hooks, terminals, price feeds, and migrations into the canonical protocol surface
10
16
 
17
+ ## Key Surfaces
18
+
19
+ - `JBController`: project launch, ruleset queueing, token setup, splits, and controller migration
20
+ - `JBMultiTerminal`: pay, payout, allowance, preview, and cash-out entrypoint
21
+ - `JBTerminalStore`: balance, surplus, fee, and reclaim accounting
22
+ - `JBDirectory`: controller and terminal routing
23
+ - `JBPermissions`: packed operator-permission registry
24
+ - `JBProjects`, `JBRulesets`, `JBPrices`, `JBFundAccessLimits`, `JBSplits`, `JBTokens`: core state and helper surfaces
25
+
11
26
  ## Journey 1: Launch A Project With The Right Initial Shape
12
27
 
13
- **Starting state:** you know who should own the project, which terminals it should use, and what the first ruleset should allow.
28
+ **Actor:** founder, deployer, or protocol integrator.
14
29
 
15
- **Success:** the project NFT exists, the initial ruleset is active, accepted terminals are installed, and downstream hooks or splits can begin working immediately.
30
+ **Intent:** create a project with the right owner, terminal, ruleset, and hook assumptions from block zero.
16
31
 
17
- **Flow**
32
+ **Preconditions**
33
+ - the team knows who should own the project, which terminals it should use, and what the first ruleset should allow
34
+
35
+ **Main Flow**
18
36
  1. Call `JBController.launchProjectFor(...)` with the owner, URI, ruleset config, terminal configs, and any split or hook metadata.
19
37
  2. `JBProjects` mints the project NFT, `JBDirectory` records controller and terminal routing, and `JBRulesets` stores the first ruleset.
20
38
  3. If the project wants ERC-20 tokens, reserved-rate behavior, or hook-driven behavior, that configuration is committed at launch instead of being inferred later.
21
39
  4. The project can now accept payments and queue future rulesets without changing project identity.
22
40
 
23
- **Failure cases that matter:** mismatched accounting contexts, wrong terminals for the target asset, invalid hook metadata, and launching with permissions or ownership assumptions that cannot be repaired cleanly later.
41
+ **Failure Modes**
42
+ - accounting contexts do not match the intended terminal asset
43
+ - hook metadata or split assumptions are invalid at launch
44
+ - ownership or permission assumptions are wrong and expensive to repair later
45
+
46
+ **Postconditions**
47
+ - the project NFT exists, the initial ruleset is active, accepted terminals are installed, and downstream hooks or splits can begin working immediately
24
48
 
25
49
  ## Journey 2: Accept A Payment And Issue The Right Token Exposure
26
50
 
27
- **Starting state:** the project has an active ruleset and a terminal that accepts the payer's asset.
51
+ **Actor:** payer or integration paying on a user's behalf.
52
+
53
+ **Intent:** settle a payment through the canonical terminal path and issue the correct token exposure.
28
54
 
29
- **Success:** treasury balances increase, hooks run in the right order, and the beneficiary receives credits or ERC-20 tokens consistent with the ruleset.
55
+ **Preconditions**
56
+ - the project has an active ruleset and a terminal that accepts the payer's asset
30
57
 
31
- **Flow**
58
+ **Main Flow**
32
59
  1. A payer calls `pay(...)` on `JBMultiTerminal`.
33
60
  2. The terminal validates the accounting context, records funds, and asks `JBTerminalStore` to derive issuance from the active ruleset.
34
61
  3. `JBController` and `JBTokens` decide whether the beneficiary gets project token credits, ERC-20s, or no issuance because weight is zero.
35
62
  4. Any configured pay hooks or data hooks run around the accounting path.
36
63
 
37
- **Edge conditions that change user experience:** paused payments, custom hook side effects, fee-on-transfer tokens, unsupported price feeds, zero-weight rulesets, and permit-based flows.
64
+ **Failure Modes**
65
+ - payments are paused or the token is unsupported for the target accounting context
66
+ - fee-on-transfer behavior or price-feed assumptions break the intended issuance path
67
+ - hooks add side effects the payer or integrator did not account for
68
+
69
+ **Postconditions**
70
+ - treasury balances increase, hooks run in the right order, and the beneficiary receives credits or ERC-20 tokens consistent with the ruleset
38
71
 
39
72
  ## Journey 3: Turn Credits Into ERC-20 Tokens Once A Project Wants A Transferable Token
40
73
 
41
- **Starting state:** users already have project token credits and the project now wants an ERC-20 representation.
74
+ **Actor:** holder or operator acting for a holder.
75
+
76
+ **Intent:** convert non-transferable project credits into ERC-20 balances once the project exposes a token.
42
77
 
43
- **Success:** the project deploys or installs its ERC-20 token and holders can claim credits into transferable balances.
78
+ **Preconditions**
79
+ - users already have project token credits
80
+ - the project now wants an ERC-20 representation
44
81
 
45
- **Flow**
82
+ **Main Flow**
46
83
  1. Deploy or set the project's ERC-20 token through `JBController`.
47
84
  2. Holders or operators call `claimTokensFor(...)` to convert credits into ERC-20 balances for a beneficiary.
48
85
  3. Future issuance can continue using the same project identity while users now interact with a standard token surface.
49
86
 
87
+ **Failure Modes**
88
+ - the wrong token is installed for the project
89
+ - integrations assume credits are automatically ERC-20 balances after token installation
90
+
91
+ **Postconditions**
92
+ - the project deploys or installs its ERC-20 token and holders can claim credits into transferable balances
93
+
50
94
  ## Journey 4: Distribute Treasury Funds Through Governed Paths
51
95
 
52
- **Starting state:** the project has terminal balances and the owner wants payouts or allowance-based withdrawals.
96
+ **Actor:** owner or authorized operator.
97
+
98
+ **Intent:** move value out of the treasury through configured payouts or allowance surfaces.
53
99
 
54
- **Success:** treasury value leaves only through configured limits, recipients, and fee logic instead of arbitrary admin withdrawals.
100
+ **Preconditions**
101
+ - the project has terminal balances
102
+ - the caller is allowed to use payout or allowance paths
55
103
 
56
- **Flow**
104
+ **Main Flow**
57
105
  1. Authorized actors call payout or allowance surfaces on the terminal.
58
106
  2. `JBFundAccessLimits` bounds how much may leave for the current ruleset cycle.
59
107
  3. `JBSplits` fans value out to beneficiaries, projects, hooks, or fee recipients as configured.
60
108
  4. `JBTerminalStore` updates balances and fee accounting so later previews and cash outs remain consistent.
61
109
 
62
- **Failure cases that matter:** stale split expectations, exceeding access limits, downstream hook failures, and assuming allowance withdrawals behave like payouts when fee treatment differs.
110
+ **Failure Modes**
111
+ - splits or access limits no longer match operator expectations
112
+ - downstream hook execution fails during payout fanout
113
+ - operators assume allowance withdrawals behave exactly like payouts when fee treatment differs
114
+
115
+ **Postconditions**
116
+ - treasury value leaves only through configured limits, recipients, and fee logic instead of arbitrary admin withdrawals
63
117
 
64
118
  ## Journey 5: Let Holders Cash Out Against Surplus
65
119
 
66
- **Starting state:** a holder owns project token exposure and the project has reclaimable surplus in some terminal.
120
+ **Actor:** holder or integrator acting for a holder.
67
121
 
68
- **Success:** the holder burns the intended amount of token exposure and receives the correct reclaim amount under the current ruleset.
122
+ **Intent:** exit project-token exposure against available terminal surplus.
69
123
 
70
- **Flow**
124
+ **Preconditions**
125
+ - a holder owns project token exposure
126
+ - the project has reclaimable surplus in some terminal
127
+
128
+ **Main Flow**
71
129
  1. The holder calls `cashOutTokensOf(...)` on the relevant terminal.
72
130
  2. `JBTerminalStore` calculates reclaim value using surplus, outstanding token supply, cash-out tax rate, and any pending reserved token effects.
73
131
  3. Cash-out hooks can modify behavior or side effects, but the core accounting remains anchored in the terminal store.
74
132
  4. Tokens burn and value exits the treasury through the terminal that actually held the asset.
75
133
 
76
- **Edge conditions that matter:** fee-free addresses, custom cash-out hooks, preview-versus-execution drift under volatile routing, and multi-terminal surplus that users may misread as single-pool liquidity.
134
+ **Failure Modes**
135
+ - fee-free or custom hook paths produce different outcomes than the holder expected
136
+ - preview-versus-execution drift appears under volatile routing or multi-terminal liquidity
137
+ - users misread multi-terminal surplus as one homogeneous pool
138
+
139
+ **Postconditions**
140
+ - the holder burns the intended amount of token exposure and receives the correct reclaim amount under the current ruleset
77
141
 
78
142
  ## Journey 6: Queue New Rulesets Without Migrating The Project
79
143
 
80
- **Starting state:** the project is live and future economics need to change.
144
+ **Actor:** owner or authorized operator.
145
+
146
+ **Intent:** change future project economics without changing the project's identity or existing balances.
81
147
 
82
- **Success:** the next ruleset activates on schedule while the project keeps the same identity, treasury, and downstream integrations.
148
+ **Preconditions**
149
+ - the project is live and future economics need to change
83
150
 
84
- **Flow**
151
+ **Main Flow**
85
152
  1. The owner or an operator with the right permission queues one or more new rulesets through `JBController`.
86
153
  2. `JBRulesets` stores the proposed future configuration and any approval hook requirements.
87
154
  3. When the active duration elapses, the next approved ruleset becomes live and future pays, payouts, and cash outs follow the new terms.
88
155
  4. Existing balances and token history remain intact because only future behavior changed.
89
156
 
90
- **Failure cases that matter:** forgetting approval hooks, queueing incompatible metadata for installed hooks, and assuming a ruleset change can retroactively repair prior accounting.
157
+ **Failure Modes**
158
+ - approval-hook requirements are forgotten or misunderstood
159
+ - queued metadata is incompatible with installed hooks
160
+ - teams assume a ruleset change can retroactively fix prior accounting
161
+
162
+ **Postconditions**
163
+ - the next ruleset activates on schedule while the project keeps the same identity, treasury, and downstream integrations
91
164
 
92
165
  ## Journey 7: Migrate A Project To New Terminal Or Controller Surfaces Deliberately
93
166
 
94
- **Starting state:** the project needs to move to a new terminal or controller path without pretending historical balances and routing do not exist.
167
+ **Actor:** owner or migration operator.
168
+
169
+ **Intent:** move a live project to new terminal or controller surfaces without corrupting balances, permissions, or routing history.
95
170
 
96
- **Success:** migration uses the protocol's explicit surfaces so balances, permissions, and future routing stay coherent.
171
+ **Preconditions**
172
+ - the project needs to move to a new terminal or controller path
173
+ - the destination surface is understood and intended
97
174
 
98
- **Flow**
175
+ **Main Flow**
99
176
  1. Confirm the active ruleset permits migration and the destination surface is the intended successor.
100
177
  2. Use `JBController.migrate(...)` and the terminal-store migration paths instead of manually repointing addresses.
101
178
  3. Recheck directory routing and accepted accounting contexts after migration completes.
102
179
 
180
+ **Failure Modes**
181
+ - migration targets are wrong or only partially configured
182
+ - operators manually repoint routing without using the protocol's migration surfaces
183
+
184
+ **Postconditions**
185
+ - balances, permissions, and future routing stay coherent after migration
186
+
103
187
  ## Journey 8: Hand Off Authority Without Handing Out Root Access
104
188
 
105
- **Starting state:** the project owner wants operators, delegates, or automation to manage only specific surfaces.
189
+ **Actor:** project owner.
190
+
191
+ **Intent:** delegate project operations narrowly instead of transferring blanket control.
106
192
 
107
- **Success:** permissions are narrow, auditable, and scoped to the actions the operator actually needs.
193
+ **Preconditions**
194
+ - the owner wants operators, delegates, or automation to manage only specific surfaces
108
195
 
109
- **Flow**
196
+ **Main Flow**
110
197
  1. The owner configures operator permissions in `JBPermissions`.
111
198
  2. Downstream calls check those packed permission bits instead of assuming project ownership.
112
199
  3. Integrations such as ownable wrappers, hook deployers, and router registries can now respect project-scoped delegation without custom ACL logic.
113
200
 
201
+ **Failure Modes**
202
+ - operators receive permissions broader than they need
203
+ - auditors assume downstream access still depends only on project ownership
204
+
205
+ **Postconditions**
206
+ - permissions are narrow, auditable, and scoped to the actions the operator actually needs
207
+
208
+ ## Trust Boundaries
209
+
210
+ - `JBTerminalStore` is the accounting truth for balances, surplus, fees, and reclaim behavior
211
+ - hooks, approval hooks, pay hooks, and cash-out hooks are trusted extension surfaces, not cosmetic plugins
212
+ - price feeds and directory routing are critical external-context surfaces inherited by many downstream repos
213
+
114
214
  ## Hand-Offs
115
215
 
116
216
  - Use [nana-permission-ids-v6](../nana-permission-ids-v6/USER_JOURNEYS.md) for the shared permission vocabulary that downstream repos import.
package/foundry.toml CHANGED
@@ -16,6 +16,8 @@ fail_on_revert = false
16
16
  [rpc_endpoints]
17
17
  ethereum = "${RPC_ETHEREUM_MAINNET}"
18
18
 
19
+ [lint]
20
+ exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
19
21
  [fmt]
20
22
  number_underscore = "thousands"
21
23
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
27
27
  },
28
28
  "dependencies": {
29
- "@bananapus/permission-ids-v6": "^0.0.15",
29
+ "@bananapus/permission-ids-v6": "^0.0.17",
30
30
  "@chainlink/contracts": "^1.5.0",
31
31
  "@openzeppelin/contracts": "^5.6.1",
32
32
  "@prb/math": "^4.1.1",
@@ -21,7 +21,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
21
21
  | `JBSplits` | Split configurations per project/ruleset/group. Packed storage for gas efficiency. |
22
22
  | `JBFundAccessLimits` | Payout limits and surplus allowances per project/ruleset/terminal/token. |
23
23
  | `JBPrices` | Price feed registry with project-specific and protocol-wide default feeds. Immutable once set. |
24
- | `JBERC20` | Cloneable ERC-20 with Votes + Permit. Owned by `JBTokens`. Deployed via `Clones.clone()`. |
24
+ | `JBERC20` | Cloneable ERC-20 with Votes + Permit + ERC-1271. Controlled by `JBTokens` via `onlyTokens`. Deployed via `Clones.clone()`. |
25
25
  | `JBFeelessAddresses` | Allowlist for fee-exempt addresses. |
26
26
  | `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`). |
27
27
  | `JBChainlinkV3SequencerPriceFeed` | L2 sequencer-aware Chainlink feed (Optimism/Arbitrum) with grace period after restart. Treats any non-zero sequencer answer as down (`answer != 0`). |
@@ -85,7 +85,8 @@ contract Deploy is Script, Sphinx {
85
85
  trustedForwarder: TRUSTED_FORWARDER
86
86
  });
87
87
  JBTokens tokens = new JBTokens{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}({
88
- directory: directory, token: new JBERC20{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}()
88
+ directory: directory,
89
+ token: new JBERC20{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}(permissions, projects)
89
90
  });
90
91
 
91
92
  new JBFundAccessLimits{salt: keccak256(abi.encode(CORE_DEPLOYMENT_NONCE))}(directory);
package/src/JBERC20.sol CHANGED
@@ -1,27 +1,49 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
- import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
4
  import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6
5
  import {ERC20Permit, Nonces} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
7
6
  import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
7
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
8
+ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
8
9
  import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
9
10
 
11
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
12
+ import {JBPermissioned} from "./abstract/JBPermissioned.sol";
13
+ import {IJBPermissions} from "./interfaces/IJBPermissions.sol";
14
+ import {IJBProjects} from "./interfaces/IJBProjects.sol";
10
15
  import {IJBToken} from "./interfaces/IJBToken.sol";
16
+ import {IJBTokens} from "./interfaces/IJBTokens.sol";
11
17
 
12
18
  /// @notice An ERC-20 token that can be used by a project in `JBTokens` and `JBController`.
13
19
  /// @dev By default, a project uses "credits" to track balances. Once a project sets their `IJBToken` using
14
20
  /// `JBController.deployERC20For(...)` or `JBController.setTokenFor(...)`, credits can be redeemed to claim tokens.
15
21
  /// @dev `JBController.deployERC20For(...)` deploys a `JBERC20` contract and sets it as the project's token.
16
- contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
22
+ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken {
17
23
  //*********************************************************************//
18
24
  // --------------------------- custom errors ------------------------- //
19
25
  //*********************************************************************//
20
26
 
21
27
  error JBERC20_AlreadyInitialized();
28
+ error JBERC20_Unauthorized();
22
29
 
23
30
  //*********************************************************************//
24
- // --------------------- internal stored properties ------------------ //
31
+ // --------------- public immutable stored properties ---------------- //
32
+ //*********************************************************************//
33
+
34
+ /// @notice The projects contract used to resolve project ownership.
35
+ IJBProjects public immutable PROJECTS;
36
+
37
+ //*********************************************************************//
38
+ // --------------------- public stored properties -------------------- //
39
+ //*********************************************************************//
40
+
41
+ /// @notice The JBTokens contract that owns this token.
42
+ /// @dev Set via `initialize` because JBERC20 is deployed before JBTokens (circular dependency).
43
+ IJBTokens public TOKENS;
44
+
45
+ //*********************************************************************//
46
+ // -------------------- private stored properties -------------------- //
25
47
  //*********************************************************************//
26
48
 
27
49
  /// @notice The token's name.
@@ -38,8 +60,29 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
38
60
 
39
61
  /// @dev Set `_name` on the implementation contract to prevent it from being initialized directly.
40
62
  /// Clones start with empty `_name`, so `initialize(...)` works only on clones.
41
- constructor() Ownable(address(this)) ERC20("invalid", "invalid") ERC20Permit("JBToken") {
63
+ /// @param permissions The permissions contract.
64
+ /// @param projects The projects contract.
65
+ constructor(
66
+ IJBPermissions permissions,
67
+ IJBProjects projects
68
+ )
69
+ ERC20("invalid", "invalid")
70
+ ERC20Permit("JBToken")
71
+ JBPermissioned(permissions)
72
+ {
42
73
  _name = "invalid";
74
+ PROJECTS = projects;
75
+ }
76
+
77
+ //*********************************************************************//
78
+ // --------------------------- modifiers ---------------------------- //
79
+ //*********************************************************************//
80
+
81
+ /// @notice Only the JBTokens contract can call this function.
82
+ // forge-lint: disable-next-line(unwrapped-modifier-logic)
83
+ modifier onlyTokens() {
84
+ if (msg.sender != address(TOKENS)) revert JBERC20_Unauthorized();
85
+ _;
43
86
  }
44
87
 
45
88
  //*********************************************************************//
@@ -47,51 +90,32 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
47
90
  //*********************************************************************//
48
91
 
49
92
  /// @notice Burn some outstanding tokens.
50
- /// @dev Can only be called by this contract's owner.
93
+ /// @dev Can only be called by the JBTokens contract.
51
94
  /// @param account The address to burn tokens from.
52
95
  /// @param amount The amount of tokens to burn, as a fixed point number with 18 decimals.
53
- function burn(address account, uint256 amount) external override onlyOwner {
96
+ function burn(address account, uint256 amount) external override onlyTokens {
54
97
  return _burn({account: account, value: amount});
55
98
  }
56
99
 
57
100
  /// @notice Mints more of this token.
58
- /// @dev Can only be called by this contract's owner.
101
+ /// @dev Can only be called by the JBTokens contract.
59
102
  /// @param account The address to mint the new tokens to.
60
103
  /// @param amount The amount of tokens to mint, as a fixed point number with 18 decimals.
61
- function mint(address account, uint256 amount) external override onlyOwner {
104
+ function mint(address account, uint256 amount) external override onlyTokens {
62
105
  return _mint({account: account, value: amount});
63
106
  }
64
107
 
65
108
  /// @notice Sets the token's name and symbol.
66
- /// @dev Can only be called by this contract's owner.
109
+ /// @dev Can only be called by the JBTokens contract.
67
110
  /// @param name_ The new name.
68
111
  /// @param symbol_ The new symbol.
69
- function setMetadata(string memory name_, string memory symbol_) external override onlyOwner {
70
- _name = name_;
71
- _symbol = symbol_;
72
- }
73
-
74
- //*********************************************************************//
75
- // ----------------------- public transactions ----------------------- //
76
- //*********************************************************************//
77
-
78
- /// @notice Initializes the token.
79
- /// @param name_ The token's name.
80
- /// @param symbol_ The token's symbol.
81
- /// @param owner The token contract's owner.
82
- function initialize(string memory name_, string memory symbol_, address owner) public override {
83
- // Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
84
- if (bytes(_name).length != 0 || bytes(name_).length == 0) revert JBERC20_AlreadyInitialized();
85
-
112
+ function setMetadata(string memory name_, string memory symbol_) external override onlyTokens {
86
113
  _name = name_;
87
114
  _symbol = symbol_;
88
-
89
- // Transfer ownership to the owner.
90
- _transferOwnership(owner);
91
115
  }
92
116
 
93
117
  //*********************************************************************//
94
- // ------------------------- external views -------------------------- //
118
+ // ----------------------- external views ---------------------------- //
95
119
  //*********************************************************************//
96
120
 
97
121
  /// @notice This token can only be added to a project when its created by the `JBTokens` contract.
@@ -99,6 +123,35 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
99
123
  return false;
100
124
  }
101
125
 
126
+ /// @notice Validates a signature on behalf of this token contract (ERC-1271).
127
+ /// @dev Allows the project owner or an operator with `SIGN_FOR_ERC20` permission to sign messages on behalf of
128
+ /// this token. Useful for Etherscan contract verification and other off-chain signature flows.
129
+ /// @param hash The hash of the data being signed.
130
+ /// @param signature The signature to validate.
131
+ /// @return magicValue `0x1626ba7e` if the signature is valid, `0xffffffff` otherwise.
132
+ function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4 magicValue) {
133
+ // Recover the signer from the signature. Return invalid if recovery fails.
134
+ // slither-disable-next-line unused-return
135
+ (address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
136
+ if (error != ECDSA.RecoverError.NoError) return 0xffffffff;
137
+
138
+ // Get the project ID this token belongs to.
139
+ uint256 projectId = TOKENS.projectIdOf(IJBToken(address(this)));
140
+
141
+ // Get the project owner (the NFT holder).
142
+ address projectOwner = PROJECTS.ownerOf(projectId);
143
+
144
+ // Valid if the signer is the project owner or has the SIGN_FOR_ERC20 permission.
145
+ if (_hasPermissionFrom({
146
+ operator: signer,
147
+ account: projectOwner,
148
+ projectId: projectId,
149
+ permissionId: JBPermissionIds.SIGN_FOR_ERC20
150
+ })) return IERC1271.isValidSignature.selector;
151
+
152
+ return 0xffffffff;
153
+ }
154
+
102
155
  //*********************************************************************//
103
156
  // -------------------------- public views --------------------------- //
104
157
  //*********************************************************************//
@@ -137,6 +190,23 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
137
190
  return super.totalSupply();
138
191
  }
139
192
 
193
+ //*********************************************************************//
194
+ // ----------------------- public transactions ----------------------- //
195
+ //*********************************************************************//
196
+
197
+ /// @notice Initializes the token.
198
+ /// @param name_ The token's name.
199
+ /// @param symbol_ The token's symbol.
200
+ /// @param tokens The JBTokens contract that manages this token.
201
+ function initialize(string memory name_, string memory symbol_, address tokens) public override {
202
+ // Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
203
+ if (bytes(_name).length != 0 || bytes(name_).length == 0) revert JBERC20_AlreadyInitialized();
204
+
205
+ _name = name_;
206
+ _symbol = symbol_;
207
+ TOKENS = IJBTokens(tokens);
208
+ }
209
+
140
210
  //*********************************************************************//
141
211
  // ---------------------- internal transactions ---------------------- //
142
212
  //*********************************************************************//
@@ -599,6 +599,8 @@ contract JBTerminalStore is IJBTerminalStore {
599
599
  JBRuleset memory ruleset = RULESETS.currentOf(projectId);
600
600
 
601
601
  // Return the amount of surplus terminal tokens that would be reclaimed.
602
+ // NOTE: This view does not run the data hook, so it cannot reflect a cross-chain totalSupply override.
603
+ // For accurate omnichain estimates, use the data hook or simulate recordCashOutFor.
602
604
  return JBCashOuts.cashOutFrom({
603
605
  surplus: surplus,
604
606
  cashOutCount: cashOutCount,
@@ -814,6 +816,8 @@ contract JBTerminalStore is IJBTerminalStore {
814
816
  if (cashOutCount > totalSupply) return 0;
815
817
 
816
818
  // Return the amount of surplus terminal tokens that would be reclaimed.
819
+ // NOTE: This view does not run the data hook, so it cannot reflect a cross-chain totalSupply override.
820
+ // For accurate omnichain estimates, use the data hook or simulate recordCashOutFor.
817
821
  return JBCashOuts.cashOutFrom({
818
822
  surplus: currentSurplus,
819
823
  cashOutCount: cashOutCount,
@@ -877,6 +881,54 @@ contract JBTerminalStore is IJBTerminalStore {
877
881
  });
878
882
  }
879
883
 
884
+ /// @notice Calls the data hook, validates noop specifications, and computes the bonding curve reclaim amount.
885
+ /// @dev Extracted from `_computeCashOutFrom` to keep it under the EVM stack depth limit (16 slots).
886
+ /// @param ruleset The current ruleset (used to resolve the data hook address).
887
+ /// @param context The fully-populated cash out context to forward to the data hook.
888
+ /// @param surplus The locally available surplus (used as a cap — can't reclaim more than what's on-chain here).
889
+ /// @return reclaimAmount The amount of surplus tokens reclaimable after the bonding curve and cap.
890
+ /// @return cashOutTaxRate The cash out tax rate returned by the data hook.
891
+ /// @return hookSpecifications The hook specifications returned by the data hook.
892
+ function _cashOutWithDataHook(
893
+ JBRuleset memory ruleset,
894
+ JBBeforeCashOutRecordedContext memory context,
895
+ uint256 surplus
896
+ )
897
+ internal
898
+ view
899
+ returns (uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] memory hookSpecifications)
900
+ {
901
+ // Ask the data hook for the effective bonding curve parameters and any hook specifications.
902
+ uint256 effectiveCashOutCount;
903
+ uint256 effectiveTotalSupply;
904
+ uint256 effectiveSurplusValue;
905
+ (cashOutTaxRate, effectiveCashOutCount, effectiveTotalSupply, effectiveSurplusValue, hookSpecifications) =
906
+ IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
907
+
908
+ // Noop specifications are informational only, so they can't also request forwarded funds.
909
+ for (uint256 i; i < hookSpecifications.length;) {
910
+ if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
911
+ revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
912
+ }
913
+ unchecked {
914
+ ++i;
915
+ }
916
+ }
917
+
918
+ // Apply the bonding curve to calculate how much of the surplus is reclaimable.
919
+ if (surplus != 0) {
920
+ reclaimAmount = JBCashOuts.cashOutFrom({
921
+ surplus: effectiveSurplusValue,
922
+ cashOutCount: effectiveCashOutCount,
923
+ totalSupply: effectiveTotalSupply,
924
+ cashOutTaxRate: cashOutTaxRate
925
+ });
926
+
927
+ // Cap at local surplus — can't reclaim more than what's locally available.
928
+ if (reclaimAmount > surplus) reclaimAmount = surplus;
929
+ }
930
+ }
931
+
880
932
  /// @notice Computes cash out results without writing state.
881
933
  /// @param terminal The terminal recording the cash out.
882
934
  /// @param holder The account that is cashing out tokens.
@@ -934,8 +986,6 @@ contract JBTerminalStore is IJBTerminalStore {
934
986
  // The terminal still burns the caller-supplied cashOutCount after pricing completes.
935
987
  // Project owners MUST audit their data hooks with the same rigor as the terminal.
936
988
 
937
- uint256 effectiveCashOutCount = cashOutCount;
938
-
939
989
  // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
940
990
  if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
941
991
  // Build the cash out context field-by-field — the struct has 11 fields, too many for a literal.
@@ -957,30 +1007,21 @@ contract JBTerminalStore is IJBTerminalStore {
957
1007
  context.beneficiaryIsFeeless = beneficiaryIsFeeless;
958
1008
  context.metadata = metadata;
959
1009
 
960
- (cashOutTaxRate, effectiveCashOutCount, effectiveTotalSupply, hookSpecifications) =
961
- IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
962
-
963
- // Noop specifications are informational only, so they can't also request forwarded funds.
964
- for (uint256 i; i < hookSpecifications.length;) {
965
- if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
966
- revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
967
- }
968
- unchecked {
969
- ++i;
970
- }
971
- }
1010
+ // Hook call + bonding curve in a helper to stay under the stack depth limit.
1011
+ (reclaimAmount, cashOutTaxRate, hookSpecifications) =
1012
+ _cashOutWithDataHook({ruleset: ruleset, context: context, surplus: surplus});
972
1013
  } else {
973
1014
  cashOutTaxRate = ruleset.cashOutTaxRate();
974
- }
975
1015
 
976
- // Apply the bonding curve to calculate how much of the surplus is reclaimable.
977
- if (surplus != 0) {
978
- reclaimAmount = JBCashOuts.cashOutFrom({
979
- surplus: surplus,
980
- cashOutCount: effectiveCashOutCount,
981
- totalSupply: effectiveTotalSupply,
982
- cashOutTaxRate: cashOutTaxRate
983
- });
1016
+ // Apply the bonding curve to calculate how much of the surplus is reclaimable.
1017
+ if (surplus != 0) {
1018
+ reclaimAmount = JBCashOuts.cashOutFrom({
1019
+ surplus: surplus,
1020
+ cashOutCount: cashOutCount,
1021
+ totalSupply: effectiveTotalSupply,
1022
+ cashOutTaxRate: cashOutTaxRate
1023
+ });
1024
+ }
984
1025
  }
985
1026
  }
986
1027
 
package/src/JBTokens.sol CHANGED
@@ -228,7 +228,7 @@ contract JBTokens is JBControlled, IJBTokens {
228
228
  });
229
229
 
230
230
  // Initialize the token.
231
- token.initialize({name: name, symbol: symbol, owner: address(this)});
231
+ token.initialize({name: name, symbol: symbol, tokens: address(this)});
232
232
  }
233
233
 
234
234
  /// @notice Mint (create) new tokens or credits.