@bananapus/core-v6 0.0.33 → 0.0.34

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/ADMINISTRATION.md CHANGED
@@ -78,10 +78,10 @@ Admin privileges and their scope in nana-core-v6.
78
78
  - **How assigned:** Set at JBFeelessAddresses deployment via the Ownable constructor. Transferable via `Ownable.transferOwnership()`.
79
79
  - **Scope:** Protocol-wide. Controls which addresses are exempt from protocol fees.
80
80
 
81
- ### JBERC20 Owner
81
+ ### JBERC20 Access Control
82
82
 
83
- - **How assigned:** Set to the `JBTokens` contract address when a project's ERC-20 is deployed or initialized.
84
- - **Scope:** Single token contract. Only the owner (JBTokens) can mint and burn tokens.
83
+ - **How assigned:** The `TOKENS` reference is set to the `JBTokens` contract address during `initialize()`. `PERMISSIONS` and `PROJECTS` are constructor immutables inherited from `JBPermissioned` and the implementation contract respectively.
84
+ - **Scope:** Single token contract. Only the `JBTokens` contract (via the `onlyTokens` modifier) can mint, burn, and update token metadata. Project owners or operators with the `SIGN_FOR_ERC20` permission can authorize ERC-1271 signatures.
85
85
 
86
86
  ### Omnichain Ruleset Operator
87
87
 
@@ -211,10 +211,11 @@ Admin privileges and their scope in nana-core-v6.
211
211
 
212
212
  | Function | Required Role | Permission ID | Scope | What It Does |
213
213
  |----------|--------------|---------------|-------|-------------|
214
- | `mint` | Contract owner (JBTokens) | N/A (onlyOwner) | Per token | Mints new tokens to an address. |
215
- | `burn` | Contract owner (JBTokens) | N/A (onlyOwner) | Per token | Burns tokens from an address. |
216
- | `initialize` | Anyone (once) | N/A | Per token | Initializes the token name, symbol, and owner. Can only be called once. |
217
- | `setMetadata` | Contract owner (JBTokens) | N/A (onlyOwner) | Per token | Updates the token's name and symbol. |
214
+ | `mint` | JBTokens contract | N/A (onlyTokens) | Per token | Mints new tokens to an address. |
215
+ | `burn` | JBTokens contract | N/A (onlyTokens) | Per token | Burns tokens from an address. |
216
+ | `initialize` | Anyone (once) | N/A | Per token | Initializes the token name, symbol, and JBTokens contract reference. Can only be called once. |
217
+ | `setMetadata` | JBTokens contract | N/A (onlyTokens) | Per token | Updates the token's name and symbol. |
218
+ | `isValidSignature` | Anyone (view) | `SIGN_FOR_ERC20` | Per token | Validates ERC-1271 signatures for project owner or permitted operators. |
218
219
 
219
220
  ### JBTerminalStore
220
221
 
package/RISKS.md CHANGED
@@ -174,7 +174,44 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
174
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
176
 
177
- ## 8. Invariants to Verify
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.
196
+
197
+ ### 8.3 Surplus allowance is ruleset-scoped, not implicit-cycle-scoped
198
+
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
178
215
 
179
216
  These should hold at all times and are the most productive targets for formal verification or invariant testing:
180
217
 
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.34",
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,50 @@
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
+ // forge-lint: disable-next-line(mixed-case-variable)
44
+ IJBTokens public TOKENS;
45
+
46
+ //*********************************************************************//
47
+ // -------------------- private stored properties -------------------- //
25
48
  //*********************************************************************//
26
49
 
27
50
  /// @notice The token's name.
@@ -38,8 +61,29 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
38
61
 
39
62
  /// @dev Set `_name` on the implementation contract to prevent it from being initialized directly.
40
63
  /// Clones start with empty `_name`, so `initialize(...)` works only on clones.
41
- constructor() Ownable(address(this)) ERC20("invalid", "invalid") ERC20Permit("JBToken") {
64
+ /// @param permissions The permissions contract.
65
+ /// @param projects The projects contract.
66
+ constructor(
67
+ IJBPermissions permissions,
68
+ IJBProjects projects
69
+ )
70
+ ERC20("invalid", "invalid")
71
+ ERC20Permit("JBToken")
72
+ JBPermissioned(permissions)
73
+ {
42
74
  _name = "invalid";
75
+ PROJECTS = projects;
76
+ }
77
+
78
+ //*********************************************************************//
79
+ // --------------------------- modifiers ---------------------------- //
80
+ //*********************************************************************//
81
+
82
+ /// @notice Only the JBTokens contract can call this function.
83
+ // forge-lint: disable-next-line(unwrapped-modifier-logic)
84
+ modifier onlyTokens() {
85
+ if (msg.sender != address(TOKENS)) revert JBERC20_Unauthorized();
86
+ _;
43
87
  }
44
88
 
45
89
  //*********************************************************************//
@@ -47,51 +91,32 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
47
91
  //*********************************************************************//
48
92
 
49
93
  /// @notice Burn some outstanding tokens.
50
- /// @dev Can only be called by this contract's owner.
94
+ /// @dev Can only be called by the JBTokens contract.
51
95
  /// @param account The address to burn tokens from.
52
96
  /// @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 {
97
+ function burn(address account, uint256 amount) external override onlyTokens {
54
98
  return _burn({account: account, value: amount});
55
99
  }
56
100
 
57
101
  /// @notice Mints more of this token.
58
- /// @dev Can only be called by this contract's owner.
102
+ /// @dev Can only be called by the JBTokens contract.
59
103
  /// @param account The address to mint the new tokens to.
60
104
  /// @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 {
105
+ function mint(address account, uint256 amount) external override onlyTokens {
62
106
  return _mint({account: account, value: amount});
63
107
  }
64
108
 
65
109
  /// @notice Sets the token's name and symbol.
66
- /// @dev Can only be called by this contract's owner.
110
+ /// @dev Can only be called by the JBTokens contract.
67
111
  /// @param name_ The new name.
68
112
  /// @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
-
113
+ function setMetadata(string memory name_, string memory symbol_) external override onlyTokens {
86
114
  _name = name_;
87
115
  _symbol = symbol_;
88
-
89
- // Transfer ownership to the owner.
90
- _transferOwnership(owner);
91
116
  }
92
117
 
93
118
  //*********************************************************************//
94
- // ------------------------- external views -------------------------- //
119
+ // ----------------------- external views ---------------------------- //
95
120
  //*********************************************************************//
96
121
 
97
122
  /// @notice This token can only be added to a project when its created by the `JBTokens` contract.
@@ -99,6 +124,35 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
99
124
  return false;
100
125
  }
101
126
 
127
+ /// @notice Validates a signature on behalf of this token contract (ERC-1271).
128
+ /// @dev Allows the project owner or an operator with `SIGN_FOR_ERC20` permission to sign messages on behalf of
129
+ /// this token. Useful for Etherscan contract verification and other off-chain signature flows.
130
+ /// @param hash The hash of the data being signed.
131
+ /// @param signature The signature to validate.
132
+ /// @return magicValue `0x1626ba7e` if the signature is valid, `0xffffffff` otherwise.
133
+ function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4 magicValue) {
134
+ // Recover the signer from the signature. Return invalid if recovery fails.
135
+ // slither-disable-next-line unused-return
136
+ (address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
137
+ if (error != ECDSA.RecoverError.NoError) return 0xffffffff;
138
+
139
+ // Get the project ID this token belongs to.
140
+ uint256 projectId = TOKENS.projectIdOf(IJBToken(address(this)));
141
+
142
+ // Get the project owner (the NFT holder).
143
+ address projectOwner = PROJECTS.ownerOf(projectId);
144
+
145
+ // Valid if the signer is the project owner or has the SIGN_FOR_ERC20 permission.
146
+ if (_hasPermissionFrom({
147
+ operator: signer,
148
+ account: projectOwner,
149
+ projectId: projectId,
150
+ permissionId: JBPermissionIds.SIGN_FOR_ERC20
151
+ })) return IERC1271.isValidSignature.selector;
152
+
153
+ return 0xffffffff;
154
+ }
155
+
102
156
  //*********************************************************************//
103
157
  // -------------------------- public views --------------------------- //
104
158
  //*********************************************************************//
@@ -137,6 +191,23 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
137
191
  return super.totalSupply();
138
192
  }
139
193
 
194
+ //*********************************************************************//
195
+ // ----------------------- public transactions ----------------------- //
196
+ //*********************************************************************//
197
+
198
+ /// @notice Initializes the token.
199
+ /// @param name_ The token's name.
200
+ /// @param symbol_ The token's symbol.
201
+ /// @param tokens The JBTokens contract that manages this token.
202
+ function initialize(string memory name_, string memory symbol_, address tokens) public override {
203
+ // Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
204
+ if (bytes(_name).length != 0 || bytes(name_).length == 0) revert JBERC20_AlreadyInitialized();
205
+
206
+ _name = name_;
207
+ _symbol = symbol_;
208
+ TOKENS = IJBTokens(tokens);
209
+ }
210
+
140
211
  //*********************************************************************//
141
212
  // ---------------------- internal transactions ---------------------- //
142
213
  //*********************************************************************//
@@ -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.
@@ -34,6 +34,34 @@ abstract contract JBPermissioned is Context, IJBPermissioned {
34
34
  // -------------------------- internal views ------------------------- //
35
35
  //*********************************************************************//
36
36
 
37
+ /// @notice Check whether an operator is the account or has the relevant permission.
38
+ /// @param operator The address to check.
39
+ /// @param account The account to allow.
40
+ /// @param projectId The project ID to check the permission under.
41
+ /// @param permissionId The required permission ID. The operator must have this permission within the specified
42
+ /// project ID.
43
+ /// @return Whether the operator is the account or has the permission.
44
+ function _hasPermissionFrom(
45
+ address operator,
46
+ address account,
47
+ uint256 projectId,
48
+ uint256 permissionId
49
+ )
50
+ internal
51
+ view
52
+ returns (bool)
53
+ {
54
+ return operator == account
55
+ || PERMISSIONS.hasPermission({
56
+ operator: operator,
57
+ account: account,
58
+ projectId: projectId,
59
+ permissionId: permissionId,
60
+ includeRoot: true,
61
+ includeWildcardProjectId: true
62
+ });
63
+ }
64
+
37
65
  /// @notice Require the message sender to be the account or have the relevant permission.
38
66
  /// @param account The account to allow.
39
67
  /// @param projectId The project ID to check the permission under.
@@ -19,7 +19,11 @@ interface IJBRulesetDataHook is IERC165 {
19
19
  /// @return cashOutTaxRate The rate determining the reclaimable amount for a given surplus and token supply.
20
20
  /// @return effectiveCashOutCount The effective token count to use for pricing the cash out. The terminal still
21
21
  /// burns the caller-supplied token count.
22
- /// @return effectiveTotalSupply The effective total supply to use for pricing the cash out.
22
+ /// @return effectiveTotalSupply The effective total supply to use for both the proportional reclaim and tax
23
+ /// calculations. For omnichain projects, this should include tokens on other chains so the tax cannot be bypassed.
24
+ /// @return effectiveSurplusValue The surplus value to use for the bonding curve calculation, denominated in the
25
+ /// same token, decimals, and currency as `context.surplus`. The terminal caps the reclaim at locally available
26
+ /// funds.
23
27
  /// @return hookSpecifications The amount and data to send to cash out hooks instead of returning to the
24
28
  /// beneficiary.
25
29
  function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
@@ -29,6 +33,7 @@ interface IJBRulesetDataHook is IERC165 {
29
33
  uint256 cashOutTaxRate,
30
34
  uint256 effectiveCashOutCount,
31
35
  uint256 effectiveTotalSupply,
36
+ uint256 effectiveSurplusValue,
32
37
  JBCashOutHookSpecification[] memory hookSpecifications
33
38
  );
34
39
 
@@ -26,11 +26,11 @@ interface IJBToken {
26
26
  /// @param amount The amount of tokens to burn.
27
27
  function burn(address account, uint256 amount) external;
28
28
 
29
- /// @notice Initializes the token with a name, symbol, and owner.
29
+ /// @notice Initializes the token with a name, symbol, and the JBTokens contract.
30
30
  /// @param name The token's name.
31
31
  /// @param symbol The token's symbol.
32
- /// @param owner The token contract's owner.
33
- function initialize(string memory name, string memory symbol, address owner) external;
32
+ /// @param tokens The JBTokens contract that manages this token.
33
+ function initialize(string memory name, string memory symbol, address tokens) external;
34
34
 
35
35
  /// @notice Mints tokens to an account.
36
36
  /// @param account The address to mint tokens to.
@@ -207,7 +207,11 @@ contract TestCashOutHooks_Local is TestBaseWorkflow {
207
207
  _DATA_HOOK,
208
208
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
209
209
  abi.encode(
210
- _ruleset.cashOutTaxRate(), _beneficiaryTokenBalance / 2, _beneficiaryTokenBalance, _specifications
210
+ _ruleset.cashOutTaxRate(),
211
+ _beneficiaryTokenBalance / 2,
212
+ _beneficiaryTokenBalance,
213
+ _nativePayAmount,
214
+ _specifications
211
215
  )
212
216
  );
213
217
 
@@ -322,7 +326,13 @@ contract TestCashOutHooks_Local is TestBaseWorkflow {
322
326
  vm.mockCall(
323
327
  _DATA_HOOK,
324
328
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
325
- abi.encode(_customCashOutTaxRate, _customCashOutCount, _customTotalSupply, _specifications)
329
+ abi.encode(
330
+ _customCashOutTaxRate,
331
+ _customCashOutCount,
332
+ _customTotalSupply,
333
+ _nativeTerminalBalance, // Same as _nativePayAmount (no payouts); avoids stack depth limit.
334
+ _specifications
335
+ )
326
336
  );
327
337
 
328
338
  _terminal.cashOutTokensOf({
@@ -261,12 +261,12 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
261
261
  uint256 cashOutCount = tokenBalance / 2;
262
262
  _hookTotalSupply = bound(_hookTotalSupply, cashOutCount, tokenBalance * 10);
263
263
 
264
- // Data hook returns: cashOutTaxRate=0, cashOutCount=half, custom totalSupply, no hook specs.
264
+ // Data hook returns: cashOutTaxRate=0, cashOutCount=half, custom totalSupply, local surplus, no hook specs.
265
265
  JBCashOutHookSpecification[] memory _emptyCashOutSpecs = new JBCashOutHookSpecification[](0);
266
266
  vm.mockCall(
267
267
  _DATA_HOOK,
268
268
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
269
- abi.encode(uint256(0), cashOutCount, _hookTotalSupply, _emptyCashOutSpecs)
269
+ abi.encode(uint256(0), cashOutCount, _hookTotalSupply, _payAmount, _emptyCashOutSpecs)
270
270
  );
271
271
 
272
272
  uint256 balanceBefore = address(this).balance;
@@ -336,7 +336,7 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
336
336
  vm.mockCall(
337
337
  _DATA_HOOK,
338
338
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
339
- abi.encode(uint256(0), cashOutCount, tokenBalance, _specs)
339
+ abi.encode(uint256(0), cashOutCount, tokenBalance, _payAmount, _specs)
340
340
  );
341
341
 
342
342
  // Mock the cash out hook call.
@@ -500,7 +500,7 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
500
500
  vm.mockCall(
501
501
  _DATA_HOOK,
502
502
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
503
- abi.encode(uint256(0), tokenBalance, tokenBalance, _specs)
503
+ abi.encode(uint256(0), tokenBalance, tokenBalance, _payAmount, _specs)
504
504
  );
505
505
 
506
506
  vm.expectRevert(
@@ -145,7 +145,13 @@ contract TestForwardedTokenConsumption_Local is TestBaseWorkflow {
145
145
  vm.mockCall(
146
146
  _DATA_HOOK,
147
147
  abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
148
- abi.encode(0, cashOutCount, _controller.totalTokenSupplyWithReservedTokensOf(_projectId), specifications)
148
+ abi.encode(
149
+ 0,
150
+ cashOutCount,
151
+ _controller.totalTokenSupplyWithReservedTokensOf(_projectId),
152
+ _PAY_AMOUNT,
153
+ specifications
154
+ )
149
155
  );
150
156
 
151
157
  vm.prank(multisig());
@@ -3,6 +3,8 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {TestBaseWorkflow} from "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBERC20} from "../src/JBERC20.sol";
6
+ import {IJBPermissions} from "../src/interfaces/IJBPermissions.sol";
7
+ import {IJBProjects} from "../src/interfaces/IJBProjects.sol";
6
8
  import {IJBRulesetApprovalHook} from "../src/interfaces/IJBRulesetApprovalHook.sol";
7
9
  import {IJBToken} from "../src/interfaces/IJBToken.sol";
8
10
  import {JBConstants} from "../src/libraries/JBConstants.sol";
@@ -15,7 +17,7 @@ import {JBTerminalConfig} from "../src/structs/JBTerminalConfig.sol";
15
17
 
16
18
  import {ERC20Votes} from "../src/JBERC20.sol";
17
19
 
18
- contract JBERC20Inheritance_Local is JBERC20, TestBaseWorkflow {
20
+ contract JBERC20Inheritance_Local is JBERC20(IJBPermissions(address(1)), IJBProjects(address(2))), TestBaseWorkflow {
19
21
  /// This test is to verify that the inheritance order of JBERC20 is correct and that it calls the
20
22
  /// `ERC20Votes._update()`
21
23
  /// forge-config: default.allow_internal_expect_revert = true
@@ -99,8 +99,8 @@ contract TestTokenFlow_Local is TestBaseWorkflow {
99
99
  });
100
100
  } else {
101
101
  // Create a new `IJBToken` and change it's owner to the `JBTokens` contract.
102
- IJBToken _newToken = IJBToken(Clones.clone(address(new JBERC20())));
103
- _newToken.initialize({name: "NewTestName", symbol: "NewTestSymbol", owner: address(_tokens)});
102
+ IJBToken _newToken = IJBToken(Clones.clone(address(new JBERC20(jbPermissions(), jbProjects()))));
103
+ _newToken.initialize({name: "NewTestName", symbol: "NewTestSymbol", tokens: address(_tokens)});
104
104
 
105
105
  // Mock the token can be added to the project.
106
106
  vm.mockCall(
@@ -260,6 +260,7 @@ contract CashOutReenterPay is TestBaseWorkflow {
260
260
  ruleset.cashOutTaxRate(), // Use the ruleset's 50% cash out tax rate.
261
261
  cashOutCount, // Number of tokens being cashed out.
262
262
  totalSupply, // Total supply for the bonding curve.
263
+ PAY_AMOUNT, // effectiveSurplusValue — full initial funding, no payouts yet.
263
264
  specifications // Our malicious hook specification.
264
265
  )
265
266
  );
@@ -475,6 +476,9 @@ contract CashOutReenterPay is TestBaseWorkflow {
475
476
  // Read the current total supply for the bonding curve calculation.
476
477
  uint256 totalSupply = _tokens.totalSupplyOf(_projectId);
477
478
 
479
+ // Read the current surplus for the bonding curve.
480
+ uint256 surplus = jbTerminalStore().balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
481
+
478
482
  // Mock the data hook to return no hook specifications (simple cashout).
479
483
  vm.mockCall(
480
484
  DATA_HOOK,
@@ -483,6 +487,7 @@ contract CashOutReenterPay is TestBaseWorkflow {
483
487
  ruleset.cashOutTaxRate(), // Pass through the ruleset's tax rate.
484
488
  cashOutCount, // Number of tokens being cashed out.
485
489
  totalSupply, // Current total supply.
490
+ surplus, // effectiveSurplusValue — current terminal balance.
486
491
  new JBCashOutHookSpecification[](0) // No hooks for this cashout.
487
492
  )
488
493
  );
@@ -0,0 +1,159 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.6;
3
+
4
+ import {TestBaseWorkflow} from "../helpers/TestBaseWorkflow.sol";
5
+ import {IJBController} from "../../src/interfaces/IJBController.sol";
6
+ import {IJBMultiTerminal} from "../../src/interfaces/IJBMultiTerminal.sol";
7
+ import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
8
+ import {JBConstants} from "../../src/libraries/JBConstants.sol";
9
+ import {JBAccountingContext} from "../../src/structs/JBAccountingContext.sol";
10
+ import {JBCurrencyAmount} from "../../src/structs/JBCurrencyAmount.sol";
11
+ import {JBFundAccessLimitGroup} from "../../src/structs/JBFundAccessLimitGroup.sol";
12
+ import {JBRulesetConfig} from "../../src/structs/JBRulesetConfig.sol";
13
+ import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
14
+ import {JBSplitGroup} from "../../src/structs/JBSplitGroup.sol";
15
+ import {JBTerminalConfig} from "../../src/structs/JBTerminalConfig.sol";
16
+
17
+ contract CodexHeldFeeRoundingTest is TestBaseWorkflow {
18
+ IJBController private _controller;
19
+ IJBMultiTerminal private _terminal;
20
+
21
+ uint256 private _projectId;
22
+ address private _projectOwner;
23
+ address private _beneficiary;
24
+
25
+ function setUp() public override {
26
+ super.setUp();
27
+
28
+ _projectOwner = multisig();
29
+ _beneficiary = beneficiary();
30
+ _terminal = jbMultiTerminal();
31
+ _controller = jbController();
32
+
33
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
34
+ reservedPercent: 0,
35
+ cashOutTaxRate: 0,
36
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
37
+ pausePay: false,
38
+ pauseCreditTransfers: false,
39
+ allowOwnerMinting: false,
40
+ allowSetCustomToken: false,
41
+ allowTerminalMigration: true,
42
+ allowSetTerminals: false,
43
+ ownerMustSendPayouts: false,
44
+ allowSetController: false,
45
+ allowAddAccountingContext: true,
46
+ allowAddPriceFeed: false,
47
+ holdFees: true,
48
+ useTotalSurplusForCashOuts: false,
49
+ useDataHookForPay: false,
50
+ useDataHookForCashOut: false,
51
+ dataHook: address(0),
52
+ metadata: 0
53
+ });
54
+
55
+ JBCurrencyAmount[] memory payoutLimits = new JBCurrencyAmount[](1);
56
+ payoutLimits[0] = JBCurrencyAmount({amount: 100, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
57
+
58
+ JBCurrencyAmount[] memory surplusAllowances = new JBCurrencyAmount[](1);
59
+ surplusAllowances[0] = JBCurrencyAmount({amount: 0, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))});
60
+
61
+ JBFundAccessLimitGroup[] memory fundAccessLimits = new JBFundAccessLimitGroup[](1);
62
+ fundAccessLimits[0] = JBFundAccessLimitGroup({
63
+ terminal: address(_terminal),
64
+ token: JBConstants.NATIVE_TOKEN,
65
+ payoutLimits: payoutLimits,
66
+ surplusAllowances: surplusAllowances
67
+ });
68
+
69
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
70
+ rulesetConfigs[0] = JBRulesetConfig({
71
+ mustStartAtOrAfter: 0,
72
+ duration: 0,
73
+ weight: 0,
74
+ weightCutPercent: 0,
75
+ approvalHook: IJBRulesetApprovalHook(address(0)),
76
+ metadata: metadata,
77
+ splitGroups: new JBSplitGroup[](0),
78
+ fundAccessLimitGroups: fundAccessLimits
79
+ });
80
+
81
+ JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
82
+ contexts[0] = JBAccountingContext({
83
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
84
+ });
85
+
86
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
87
+ terminalConfigs[0] = JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: contexts});
88
+
89
+ // Project 1 is the fee project.
90
+ _controller.launchProjectFor({
91
+ owner: _projectOwner,
92
+ projectUri: "fee-project",
93
+ rulesetConfigurations: rulesetConfigs,
94
+ terminalConfigurations: terminalConfigs,
95
+ memo: ""
96
+ });
97
+
98
+ _projectId = _controller.launchProjectFor({
99
+ owner: _projectOwner,
100
+ projectUri: "project",
101
+ rulesetConfigurations: rulesetConfigs,
102
+ terminalConfigurations: terminalConfigs,
103
+ memo: ""
104
+ });
105
+ }
106
+
107
+ function test_partialHeldFeeRepaymentCanEraseRemainingFee() external {
108
+ // Seed the project with enough balance to send a payout that holds fees.
109
+ _terminal.pay{value: 100}({
110
+ projectId: _projectId,
111
+ amount: 100,
112
+ token: JBConstants.NATIVE_TOKEN,
113
+ beneficiary: _beneficiary,
114
+ minReturnedTokens: 0,
115
+ memo: "",
116
+ metadata: new bytes(0)
117
+ });
118
+
119
+ _terminal.sendPayoutsOf({
120
+ projectId: _projectId,
121
+ token: JBConstants.NATIVE_TOKEN,
122
+ amount: 40,
123
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
124
+ minTokensPaidOut: 0
125
+ });
126
+
127
+ // 40 gross produces a 1 wei fee and 39 wei net payout.
128
+ assertEq(address(_projectOwner).balance, 39);
129
+
130
+ vm.prank(_projectOwner);
131
+ _terminal.addToBalanceOf{value: 1}({
132
+ projectId: _projectId,
133
+ token: JBConstants.NATIVE_TOKEN,
134
+ amount: 1,
135
+ shouldReturnHeldFees: true,
136
+ memo: "",
137
+ metadata: new bytes(0)
138
+ });
139
+
140
+ // After repaying only 1 wei of the 39 wei payout, the fee should still be owed in full.
141
+ uint256 feeProjectBalanceBefore = jbTerminalStore().balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
142
+ assertEq(feeProjectBalanceBefore, 0);
143
+
144
+ vm.warp(block.timestamp + 2_419_200);
145
+ _terminal.processHeldFeesOf(_projectId, JBConstants.NATIVE_TOKEN, 10);
146
+
147
+ uint256 feeProjectBalanceAfter = jbTerminalStore().balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
148
+ uint256 projectBalanceAfter =
149
+ jbTerminalStore().balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
150
+
151
+ // The fee project never receives the original 1 wei fee.
152
+ assertEq(feeProjectBalanceAfter, 0);
153
+ // The payer project only gets its explicit top-up recorded.
154
+ assertEq(projectBalanceAfter, 61);
155
+ // One wei remains stranded in the terminal: actual native balance exceeds tracked balances.
156
+ assertEq(address(_terminal).balance, 62);
157
+ assertEq(address(_terminal).balance - (feeProjectBalanceAfter + projectBalanceAfter), 1);
158
+ }
159
+ }
@@ -293,7 +293,7 @@ contract TestBaseWorkflow is JBTest, DeployPermit2 {
293
293
  _jbPermissions = new JBPermissions(_trustedForwarder);
294
294
  _jbProjects = new JBProjects(_multisig, address(0), _trustedForwarder);
295
295
  _jbDirectory = new JBDirectory(_jbPermissions, _jbProjects, _multisig);
296
- _jbErc20 = new JBERC20();
296
+ _jbErc20 = new JBERC20(_jbPermissions, _jbProjects);
297
297
  _jbTokens = new JBTokens(_jbDirectory, _jbErc20);
298
298
  _jbRulesets = new JBRulesets(_jbDirectory);
299
299
  _jbPrices = new JBPrices(_jbDirectory, _jbPermissions, _jbProjects, _multisig, _trustedForwarder);
@@ -3,6 +3,8 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
5
5
  import {JBERC20} from "../../../../src/JBERC20.sol";
6
+ import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
7
+ import {IJBProjects} from "../../../../src/interfaces/IJBProjects.sol";
6
8
  import {IJBToken} from "../../../../src/interfaces/IJBToken.sol";
7
9
  import {JBTest} from "../../../helpers/JBTest.sol";
8
10
 
@@ -11,7 +13,10 @@ Contract that deploys a target contract with other mock contracts to satisfy the
11
13
  Tests relative to this contract will be dependent on mock calls/emits and stdStorage.
12
14
  */
13
15
  contract JBERC20Setup is JBTest {
14
- address _owner = makeAddr("owner");
16
+ // Mocks
17
+ address _tokens = makeAddr("tokens");
18
+ IJBProjects _projects = IJBProjects(makeAddr("projects"));
19
+ IJBPermissions _permissions = IJBPermissions(makeAddr("permissions"));
15
20
 
16
21
  // Implementation (constructor sets _name = "invalid", cannot be initialized)
17
22
  IJBToken public _implementation;
@@ -20,8 +25,8 @@ contract JBERC20Setup is JBTest {
20
25
  IJBToken public _erc20;
21
26
 
22
27
  function erc20Setup() public virtual {
23
- // Deploy the implementation
24
- _implementation = new JBERC20();
28
+ // Deploy the implementation with immutable permissions and projects
29
+ _implementation = new JBERC20(_permissions, _projects);
25
30
 
26
31
  // Clone it — clones start with empty storage, so initialize() works
27
32
  _erc20 = IJBToken(Clones.clone(address(_implementation)));
@@ -4,7 +4,6 @@ pragma solidity 0.8.28;
4
4
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
5
5
  import {JBERC20} from "../../../../src/JBERC20.sol";
6
6
  import {JBERC20Setup} from "./JBERC20Setup.sol";
7
- import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
8
7
 
9
8
  contract TestInitialize_Local is JBERC20Setup {
10
9
  string _name = "Nana";
@@ -17,21 +16,21 @@ contract TestInitialize_Local is JBERC20Setup {
17
16
  function test_ImplementationCannotBeInitialized() external {
18
17
  // The implementation has _name = "invalid" set in constructor, so initialize() must revert.
19
18
  vm.expectRevert(JBERC20.JBERC20_AlreadyInitialized.selector);
20
- _implementation.initialize(_name, _symbol, _owner);
19
+ _implementation.initialize(_name, _symbol, _tokens);
21
20
  }
22
21
 
23
22
  function test_WhenANameIsAlreadySet() external {
24
23
  // it will revert
25
24
 
26
- _erc20.initialize(_name, _symbol, _owner);
25
+ _erc20.initialize(_name, _symbol, _tokens);
27
26
 
28
- // ensure ownership transferred
29
- address newOwner = Ownable(address(_erc20)).owner();
30
- assertEq(newOwner, _owner);
27
+ // ensure TOKENS is set
28
+ address setTokens = address(JBERC20(address(_erc20)).TOKENS());
29
+ assertEq(setTokens, _tokens);
31
30
 
32
31
  // will fail as internal name is no longer zero length
33
32
  vm.expectRevert();
34
- _erc20.initialize(_name, _symbol, _owner);
33
+ _erc20.initialize(_name, _symbol, _tokens);
35
34
  }
36
35
 
37
36
  function test_WhenName_EQNothing() external {
@@ -39,17 +38,17 @@ contract TestInitialize_Local is JBERC20Setup {
39
38
 
40
39
  // will fail as internal name is no longer than zero length
41
40
  vm.expectRevert();
42
- _erc20.initialize("", _symbol, _owner);
41
+ _erc20.initialize("", _symbol, _tokens);
43
42
  }
44
43
 
45
44
  function test_WhenNameIsValidAndNotAlreadySet() external {
46
- // it will set the name and symbol and transfer ownership
45
+ // it will set the name, symbol, and store references
47
46
 
48
- _erc20.initialize(_name, _symbol, _owner);
47
+ _erc20.initialize(_name, _symbol, _tokens);
49
48
 
50
- // ensure ownership transferred
51
- address newOwner = Ownable(address(_erc20)).owner();
52
- assertEq(newOwner, _owner);
49
+ // ensure TOKENS is set
50
+ address setTokens = address(JBERC20(address(_erc20)).TOKENS());
51
+ assertEq(setTokens, _tokens);
53
52
 
54
53
  // name is set
55
54
  string memory _setName = IERC20Metadata(address(_erc20)).name();
@@ -15,7 +15,7 @@ contract TestName_Local is JBERC20Setup {
15
15
 
16
16
  function test_WhenANameIsSet() external {
17
17
  // it will return the name
18
- _erc20.initialize("NANAPUS", "NANA", _owner);
18
+ _erc20.initialize("NANAPUS", "NANA", _tokens);
19
19
 
20
20
  string memory _setName = _token.name();
21
21
  assertEq(_setName, "NANAPUS");
@@ -6,6 +6,7 @@ import {JBERC20Setup} from "./JBERC20Setup.sol";
6
6
  import {SigUtils} from "./SigUtils.sol";
7
7
 
8
8
  contract TestNonces_Local is JBERC20Setup {
9
+ address _user = makeAddr("user");
9
10
  IERC20Permit _token;
10
11
  SigUtils sigUtils;
11
12
 
@@ -30,7 +31,7 @@ contract TestNonces_Local is JBERC20Setup {
30
31
  function test_WhenAUserHasNotCalledPermit() external view {
31
32
  // it will return zero
32
33
 
33
- uint256 _nonce = _token.nonces(_owner);
34
+ uint256 _nonce = _token.nonces(_user);
34
35
 
35
36
  assertEq(_nonce, 0);
36
37
  }
@@ -16,7 +16,7 @@ contract TestSymbol_Local is JBERC20Setup {
16
16
  function test_WhenASymbolIsSet() external {
17
17
  // it will return a non-empty string
18
18
 
19
- _erc20.initialize("NANAPUS", "NANA", _owner);
19
+ _erc20.initialize("NANAPUS", "NANA", _tokens);
20
20
 
21
21
  string memory _setSymbol = _token.symbol();
22
22
  assertEq(_setSymbol, "NANA");
@@ -453,7 +453,7 @@ contract TestPreviewCashOutFor_Local is JBTerminalStoreSetup {
453
453
  mockExpect(
454
454
  address(_dataHook),
455
455
  abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
456
- abi.encode(0, 10e18, _totalSupply, _spec)
456
+ abi.encode(0, 10e18, _totalSupply, 3e18, _spec)
457
457
  );
458
458
 
459
459
  (, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] memory hookSpecifications) = _store.previewCashOutFrom({
@@ -417,7 +417,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
417
417
  mockExpect(
418
418
  address(_dataHook),
419
419
  abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
420
- abi.encode(0, 1e18, _totalSupply, _spec)
420
+ abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
421
421
  );
422
422
 
423
423
  uint256 balanceBefore = _store.balanceOf(address(this), _projectId, _accountingContexts.token);
@@ -521,7 +521,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
521
521
  mockExpect(
522
522
  address(_dataHook),
523
523
  abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
524
- abi.encode(0, 1e18, _totalSupply, _spec)
524
+ abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
525
525
  );
526
526
 
527
527
  (, uint256 reclaimed,,) = _store.recordCashOutFor({
@@ -576,7 +576,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
576
576
  mockExpect(
577
577
  address(_dataHook),
578
578
  abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
579
- abi.encode(0, 1e18, _totalSupply, _spec)
579
+ abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
580
580
  );
581
581
 
582
582
  vm.expectRevert(abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_NoopHookSpecHasAmount.selector, 1));
@@ -631,7 +631,7 @@ contract TestRecordCashOutsFor_Local is JBTerminalStoreSetup {
631
631
  mockExpect(
632
632
  address(_dataHook),
633
633
  abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
634
- abi.encode(0, 1e18, _totalSupply, _spec)
634
+ abi.encode(0, 1e18, _totalSupply, 3e18, _spec)
635
635
  );
636
636
 
637
637
  vm.expectRevert(abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_NoopHookSpecHasAmount.selector, 1));
@@ -4,6 +4,8 @@ pragma solidity 0.8.28;
4
4
  import {JBERC20} from "../../../../src/JBERC20.sol";
5
5
  import {JBTokens} from "../../../../src/JBTokens.sol";
6
6
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
7
+ import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
8
+ import {IJBProjects} from "../../../../src/interfaces/IJBProjects.sol";
7
9
  import {IJBToken} from "../../../../src/interfaces/IJBToken.sol";
8
10
  import {IJBTokens} from "../../../../src/interfaces/IJBTokens.sol";
9
11
  import {JBTest} from "../../../helpers/JBTest.sol";
@@ -15,6 +17,8 @@ Tests relative to this contract will be dependent on mock calls/emits and stdSto
15
17
  contract JBTokensSetup is JBTest {
16
18
  // Mocks
17
19
  IJBDirectory public directory = IJBDirectory(makeAddr("directory"));
20
+ IJBPermissions public permissions = IJBPermissions(makeAddr("permissions"));
21
+ IJBProjects public projects = IJBProjects(makeAddr("projects"));
18
22
  IJBToken public jbToken;
19
23
 
20
24
  // Target Contract
@@ -22,7 +26,7 @@ contract JBTokensSetup is JBTest {
22
26
 
23
27
  function tokensSetup() public virtual {
24
28
  // Instantiate the contract being tested
25
- jbToken = new JBERC20();
29
+ jbToken = new JBERC20(permissions, projects);
26
30
  _tokens = new JBTokens(directory, jbToken);
27
31
  }
28
32
  }