@bananapus/core-v6 0.0.32 → 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.32",
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
  //*********************************************************************//