@bananapus/core-v6 0.0.10 → 0.0.12

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.
@@ -0,0 +1,327 @@
1
+ # Administration
2
+
3
+ Admin privileges and their scope in nana-core-v6.
4
+
5
+ ## Roles
6
+
7
+ ### Project Owner
8
+
9
+ - **How assigned:** Holds the ERC-721 NFT minted by `JBProjects.createFor()`. Transferable via standard ERC-721 transfer.
10
+ - **Scope:** Controls a single project. The owner's address is `PROJECTS.ownerOf(projectId)`. Operators can be granted subsets of the owner's permissions via `JBPermissions`.
11
+
12
+ ### Controller
13
+
14
+ - **How assigned:** Set via `JBDirectory.setControllerOf()`. The controller is stored as `controllerOf[projectId]` in JBDirectory. Typically a `JBController` instance.
15
+ - **Scope:** Manages a single project's rulesets, tokens, splits, and fund access limits. Contracts gated by `onlyControllerOf(projectId)` (from `JBControlled`) only accept calls from the address registered as the project's controller in the directory.
16
+
17
+ ### Terminal
18
+
19
+ - **How assigned:** Set via `JBDirectory.setTerminalsOf()`. Stored in the directory's `_terminalsOf[projectId]` array.
20
+ - **Scope:** Manages fund inflows and outflows for a single project. Terminal identity is verified via `DIRECTORY.isTerminalOf()`. Terminals interact with `JBTerminalStore` using `msg.sender`-scoped bookkeeping -- each terminal can only modify its own balances.
21
+
22
+ ### Operator
23
+
24
+ - **How assigned:** Granted permissions by any address via `JBPermissions.setPermissionsFor()`. Permissions are stored as a packed `uint256` bitmap per `(operator, account, projectId)` tuple.
25
+ - **Scope:** Can execute specific functions on behalf of the account that granted them permissions, scoped to a specific project ID (or all projects if `projectId = 0` wildcard is used).
26
+
27
+ ### ROOT Permission Holder
28
+
29
+ - **How assigned:** Granted the ROOT permission (ID 1) by an account via `JBPermissions`. ROOT on a specific project grants all permissions for that project. ROOT cannot be granted on the wildcard project ID (0) to prevent unlimited cross-project access.
30
+ - **Scope:** ROOT holders for a project can do anything the project owner can do for that project. ROOT holders can also grant non-ROOT permissions to other operators for the same project but cannot grant ROOT to others or set wildcard permissions.
31
+
32
+ ### Directory Owner
33
+
34
+ - **How assigned:** Set at JBDirectory deployment via the Ownable constructor. Transferable via `Ownable.transferOwnership()`.
35
+ - **Scope:** Protocol-wide. Controls which addresses are allowed to set a project's first controller (`isAllowedToSetFirstController`).
36
+
37
+ ### JBProjects Owner
38
+
39
+ - **How assigned:** Set at JBProjects deployment via the Ownable constructor. Transferable via `Ownable.transferOwnership()`.
40
+ - **Scope:** Protocol-wide. Can set the `tokenUriResolver` that resolves project NFT metadata URIs.
41
+
42
+ ### JBPrices Owner
43
+
44
+ - **How assigned:** Set at JBPrices deployment via the Ownable constructor. Transferable via `Ownable.transferOwnership()`.
45
+ - **Scope:** Protocol-wide. Can add default price feeds (project ID 0) that apply to all projects as fallback.
46
+
47
+ ### JBFeelessAddresses Owner
48
+
49
+ - **How assigned:** Set at JBFeelessAddresses deployment via the Ownable constructor. Transferable via `Ownable.transferOwnership()`.
50
+ - **Scope:** Protocol-wide. Controls which addresses are exempt from protocol fees.
51
+
52
+ ### JBERC20 Owner
53
+
54
+ - **How assigned:** Set to the `JBTokens` contract address when a project's ERC-20 is deployed or initialized.
55
+ - **Scope:** Single token contract. Only the owner (JBTokens) can mint and burn tokens.
56
+
57
+ ### Omnichain Ruleset Operator
58
+
59
+ - **How assigned:** Immutable address set in the `JBController` constructor as `OMNICHAIN_RULESET_OPERATOR`.
60
+ - **Scope:** Can call `launchRulesetsFor` and `queueRulesetsOf` on any project, bypassing owner permission checks. This allows the omnichain deployer to synchronize rulesets across chains.
61
+
62
+ ### Data Hook
63
+
64
+ - **How assigned:** Specified in a ruleset's metadata as the `dataHook` address. Active only during the ruleset it is configured for.
65
+ - **Scope:** Can mint tokens via `JBController.mintTokensOf()` when the data hook reports `hasMintPermissionFor()` returns true. Also controls pay/cashout hook specifications returned from `JBTerminalStore`.
66
+
67
+ ### Fee Beneficiary (Project ID 1)
68
+
69
+ - **How assigned:** Hardcoded as `_FEE_BENEFICIARY_PROJECT_ID = 1` in JBMultiTerminal.
70
+ - **Scope:** Receives all protocol fees (2.5%) from payouts, surplus allowance usage, and cash outs with non-zero tax rates. This is the first project created during deployment.
71
+
72
+ ## Privileged Functions
73
+
74
+ ### JBPermissions
75
+
76
+ | Function | Required Role | Permission ID | Scope | What It Does |
77
+ |----------|--------------|---------------|-------|-------------|
78
+ | `setPermissionsFor` | Account owner, or ROOT operator for that account+project | ROOT (1) | Per account + project | Sets the permission bitmap for an operator. ROOT operators can set non-ROOT permissions for the same project but cannot grant ROOT or set wildcard project permissions. |
79
+
80
+ ### JBProjects
81
+
82
+ | Function | Required Role | Permission ID | Scope | What It Does |
83
+ |----------|--------------|---------------|-------|-------------|
84
+ | `setTokenUriResolver` | Contract owner | N/A (onlyOwner) | Protocol-wide | Sets the contract that resolves token URIs for all project NFTs. |
85
+ | `createFor` | Anyone | N/A | N/A | Creates a new project NFT. No permission required -- anyone can create a project for any owner. |
86
+
87
+ ### JBDirectory
88
+
89
+ | Function | Required Role | Permission ID | Scope | What It Does |
90
+ |----------|--------------|---------------|-------|-------------|
91
+ | `setControllerOf` | Project owner or operator, OR an `isAllowedToSetFirstController` address (for first controller only) | SET_CONTROLLER (14) | Per project | Sets or migrates a project's controller. Also requires the current ruleset's `allowSetController` flag to be true (unless setting the first controller). Triggers migration lifecycle hooks on both old and new controllers. |
92
+ | `setIsAllowedToSetFirstController` | Contract owner | N/A (onlyOwner) | Protocol-wide | Adds or removes an address from the allowlist of addresses that can set a project's first controller. |
93
+ | `setPrimaryTerminalOf` | Project owner or operator | SET_PRIMARY_TERMINAL (16) | Per project | Sets which terminal is the default for a given token. Adds the terminal to the project if not already present. |
94
+ | `setTerminalsOf` | Project owner, operator, or the project's controller | SET_TERMINALS (15) | Per project | Replaces the entire list of terminals for a project. If the caller is not the controller, the ruleset must have `allowSetTerminals` enabled. |
95
+
96
+ ### JBController
97
+
98
+ | Function | Required Role | Permission ID | Scope | What It Does |
99
+ |----------|--------------|---------------|-------|-------------|
100
+ | `addPriceFeed` | Project owner or operator | ADD_PRICE_FEED (19) | Per project | Adds a price feed for a project. Requires the ruleset's `allowAddPriceFeed` flag. Price feeds are immutable once set. |
101
+ | `burnTokensOf` | Token holder, operator with BURN_TOKENS, or a project terminal | BURN_TOKENS (11) | Per project | Burns tokens or credits from a holder's balance. Terminals can burn without explicit permission (for cash outs). |
102
+ | `claimTokensFor` | Credit holder or operator | CLAIM_TOKENS (12) | Per project | Redeems internal credits for ERC-20 tokens. |
103
+ | `deployERC20For` | Project owner or operator | DEPLOY_ERC20 (8) | Per project | Deploys a new ERC-20 token contract for the project. Can only be called once per project. |
104
+ | `launchProjectFor` | Anyone | N/A | N/A | Creates a new project, sets this controller, configures terminals, and queues initial rulesets. No permission required. |
105
+ | `launchRulesetsFor` | Project owner, operator, or OMNICHAIN_RULESET_OPERATOR | LAUNCH_RULESETS (3) + SET_TERMINALS (15) | Per project | Queues initial rulesets and configures terminals for an existing project that has no rulesets yet. Also sets this contract as the project's controller. |
106
+ | `mintTokensOf` | Project owner, operator, terminal, or data hook (with mint permission) | MINT_TOKENS (10) | Per project | Mints new tokens. If the caller is not a terminal or data hook, the ruleset must have `allowOwnerMinting` enabled. |
107
+ | `queueRulesetsOf` | Project owner, operator, or OMNICHAIN_RULESET_OPERATOR | QUEUE_RULESETS (2) | Per project | Queues new rulesets at the end of the project's ruleset queue. |
108
+ | `sendReservedTokensToSplitsOf` | Anyone | N/A | Per project | Distributes accumulated reserved tokens to the project's reserved token split group. No permission required -- anyone can trigger this. |
109
+ | `setSplitGroupsOf` | Project owner or operator | SET_SPLIT_GROUPS (18) | Per project | Sets split groups for a project. Must preserve any currently locked splits. |
110
+ | `setTokenFor` | Project owner or operator | SET_TOKEN (9) | Per project | Assigns an external ERC-20 token to the project. Requires the ruleset's `allowSetCustomToken` flag. Can only be called once (before any token is set). |
111
+ | `setUriOf` | Project owner or operator | SET_PROJECT_URI (7) | Per project | Updates the project's metadata URI. |
112
+ | `transferCreditsFrom` | Credit holder or operator | TRANSFER_CREDITS (13) | Per project | Transfers internal token credits between addresses. Requires the ruleset's `pauseCreditTransfers` flag to be false. |
113
+ | `migrate` | JBDirectory only | N/A (msg.sender == DIRECTORY) | Per project | Called by the directory during controller migration. Reverts if there are pending reserved tokens. |
114
+ | `beforeReceiveMigrationFrom` | JBDirectory only | N/A (msg.sender == DIRECTORY) | Per project | Called before migration to prepare the new controller. Copies metadata URI and distributes pending reserved tokens from the old controller. |
115
+ | `afterReceiveMigrationFrom` | JBDirectory only | N/A (msg.sender == DIRECTORY) | Per project | Called after migration completes. Currently a no-op. |
116
+ | `executePayReservedTokenToTerminal` | Self only | N/A (msg.sender == address(this)) | Internal | Pays a terminal with reserved tokens. Called internally via try-catch during reserved token distribution. |
117
+
118
+ ### JBMultiTerminal
119
+
120
+ | Function | Required Role | Permission ID | Scope | What It Does |
121
+ |----------|--------------|---------------|-------|-------------|
122
+ | `addAccountingContextsFor` | Project owner, operator, or the project's controller | ADD_ACCOUNTING_CONTEXTS (20) | Per project | Adds tokens that the terminal will accept for a project. Requires the ruleset's `allowAddAccountingContext` flag (if a ruleset exists). |
123
+ | `cashOutTokensOf` | Token holder or operator | CASH_OUT_TOKENS (4) | Per project | Cashes out project tokens for a share of the project's surplus. Fees are charged unless the beneficiary is feeless or the cash out tax rate is zero. |
124
+ | `migrateBalanceOf` | Project owner or operator | MIGRATE_TERMINAL (6) | Per project | Migrates a project's balance from this terminal to another. The destination terminal must accept the same token. The ruleset must have `allowTerminalMigration` enabled (checked in JBTerminalStore). |
125
+ | `sendPayoutsOf` | Anyone (unless `ownerMustSendPayouts` is set) | SEND_PAYOUTS (5) if `ownerMustSendPayouts` | Per project | Sends payouts to the project's payout split group up to the payout limit. Anyone can call unless the ruleset has `ownerMustSendPayouts` enabled, which requires the project owner or an operator with SEND_PAYOUTS permission. |
126
+ | `useAllowanceOf` | Project owner or operator | USE_ALLOWANCE (17) | Per project | Withdraws funds from the project's surplus up to the surplus allowance. Fees are charged unless the owner or beneficiary is feeless. |
127
+ | `pay` | Anyone | N/A | N/A | Pays a project with tokens. No permission required. |
128
+ | `addToBalanceOf` | Anyone | N/A | N/A | Adds funds to a project's balance without minting tokens. Can optionally return held fees. No permission required. |
129
+ | `processHeldFeesOf` | Anyone | N/A | Per project | Processes held fees that have passed their 28-day unlock period. No permission required. |
130
+ | `executePayout` | Self only | N/A (msg.sender == address(this)) | Internal | Executes a single payout to a split. Called internally via try-catch during payout distribution. |
131
+ | `executeProcessFee` | Self only | N/A (msg.sender == address(this)) | Internal | Processes a fee payment to the fee beneficiary project. Called internally via try-catch. |
132
+ | `executeTransferTo` | Self only | N/A (msg.sender == address(this)) | Internal | Transfers tokens to an address. Called internally via try-catch during payout leftover distribution. |
133
+
134
+ ### JBTokens
135
+
136
+ | Function | Required Role | Permission ID | Scope | What It Does |
137
+ |----------|--------------|---------------|-------|-------------|
138
+ | `burnFrom` | Project's controller | N/A (onlyControllerOf) | Per project | Burns tokens and/or credits from a holder. Credits are burned first, then ERC-20 tokens. |
139
+ | `claimTokensFor` | Project's controller | N/A (onlyControllerOf) | Per project | Converts credits to ERC-20 tokens for a holder. |
140
+ | `deployERC20For` | Project's controller | N/A (onlyControllerOf) | Per project | Deploys a cloned JBERC20 token for the project. |
141
+ | `mintFor` | Project's controller | N/A (onlyControllerOf) | Per project | Mints new tokens (ERC-20 if deployed) or credits for a holder. |
142
+ | `setTokenFor` | Project's controller | N/A (onlyControllerOf) | Per project | Sets an external ERC-20 token for the project. Cannot be changed once set. |
143
+ | `transferCreditsFrom` | Project's controller | N/A (onlyControllerOf) | Per project | Transfers credits between addresses. |
144
+
145
+ ### JBSplits
146
+
147
+ | Function | Required Role | Permission ID | Scope | What It Does |
148
+ |----------|--------------|---------------|-------|-------------|
149
+ | `setSplitGroupsOf` | Project's controller, OR the address whose first 160 bits match the group ID (for self-namespaced splits) | N/A (onlyControllerOf or msg.sender namespace) | Per project | Sets split groups for a project/ruleset. Must preserve any currently locked splits. Percentage total per group must not exceed 100%. |
150
+
151
+ ### JBFundAccessLimits
152
+
153
+ | Function | Required Role | Permission ID | Scope | What It Does |
154
+ |----------|--------------|---------------|-------|-------------|
155
+ | `setFundAccessLimitsFor` | Project's controller | N/A (onlyControllerOf) | Per project | Sets payout limits and surplus allowances for a project's ruleset. Limits must be in strictly increasing currency order. |
156
+
157
+ ### JBRulesets
158
+
159
+ | Function | Required Role | Permission ID | Scope | What It Does |
160
+ |----------|--------------|---------------|-------|-------------|
161
+ | `queueFor` | Project's controller | N/A (onlyControllerOf) | Per project | Queues a new ruleset with specified duration, weight, weight cut percent, approval hook, and metadata. |
162
+ | `updateRulesetWeightCache` | Anyone | N/A | Per project | Updates the cached weight for a ruleset to avoid excessive iteration. No permission required -- anyone can call this maintenance function. |
163
+
164
+ ### JBPrices
165
+
166
+ | Function | Required Role | Permission ID | Scope | What It Does |
167
+ |----------|--------------|---------------|-------|-------------|
168
+ | `addPriceFeedFor` | Contract owner (for project ID 0 defaults) or project's controller (for project-specific feeds) | N/A (onlyOwner or onlyControllerOf) | Protocol-wide or per project | Adds an immutable price feed for a currency pair. Default feeds (project 0) apply as fallback for all projects. |
169
+
170
+ ### JBFeelessAddresses
171
+
172
+ | Function | Required Role | Permission ID | Scope | What It Does |
173
+ |----------|--------------|---------------|-------|-------------|
174
+ | `setFeelessAddress` | Contract owner | N/A (onlyOwner) | Protocol-wide | Marks an address as feeless or not. Feeless addresses do not incur the 2.5% protocol fee on payouts, surplus allowance usage, or cash outs. |
175
+
176
+ ### JBERC20
177
+
178
+ | Function | Required Role | Permission ID | Scope | What It Does |
179
+ |----------|--------------|---------------|-------|-------------|
180
+ | `mint` | Contract owner (JBTokens) | N/A (onlyOwner) | Per token | Mints new tokens to an address. |
181
+ | `burn` | Contract owner (JBTokens) | N/A (onlyOwner) | Per token | Burns tokens from an address. |
182
+ | `initialize` | Anyone (once) | N/A | Per token | Initializes the token name, symbol, and owner. Can only be called once. |
183
+
184
+ ### JBTerminalStore
185
+
186
+ JBTerminalStore has no explicit access control modifiers. Instead, it uses `msg.sender`-scoped storage -- each terminal can only read and write its own balance slots. The functions are designed to be called by terminal contracts:
187
+
188
+ | Function | Implicit Caller | What It Does |
189
+ |----------|----------------|-------------|
190
+ | `recordAddedBalanceFor` | Any address (terminal) | Increments the caller's recorded balance for a project/token. |
191
+ | `recordCashOutFor` | Any address (terminal) | Records a cash out, calculating reclaim amounts via bonding curve. Decrements the caller's balance. |
192
+ | `recordPaymentFrom` | Any address (terminal) | Records a payment, calculating token issuance via weight. Increments the caller's balance. |
193
+ | `recordPayoutFor` | Any address (terminal) | Records a payout against payout limits. Decrements the caller's balance. |
194
+ | `recordTerminalMigration` | Any address (terminal) | Records a full balance migration. Requires the ruleset's `allowTerminalMigration` flag. Zeros out the caller's balance. |
195
+ | `recordUsedAllowanceOf` | Any address (terminal) | Records surplus allowance usage. Decrements the caller's balance. |
196
+
197
+ ## Permission System
198
+
199
+ JBPermissions implements a 256-bit packed permission bitmap system:
200
+
201
+ - **256 permission slots**: Each bit in a `uint256` represents one permission. Bit 0 is reserved (cannot be set). Bits 1-255 are available for permission IDs.
202
+ - **ROOT permission (ID 1)**: Acts as a superuser for the granted project. A ROOT operator passes all permission checks for that project via `includeRoot: true`.
203
+ - **Wildcard project ID (0)**: Granting a permission on project ID 0 makes it apply to all projects for that account. ROOT cannot be granted on the wildcard project ID to prevent unlimited cross-project access.
204
+ - **Operator delegation**: Any address can grant any other address specific permissions. The `_requirePermissionFrom` check passes if `msg.sender == account` OR if the sender has the required permission (with ROOT fallback and wildcard fallback).
205
+ - **Override pattern**: `_requirePermissionAllowingOverrideFrom` adds an `alsoGrantAccessIf` boolean that bypasses the permission check entirely when true. This is used to allow terminals, controllers, and data hooks to call privileged functions without explicit operator permissions.
206
+
207
+ ### Permission IDs Used in nana-core-v6
208
+
209
+ | ID | Name | Function It Gates |
210
+ |----|------|-------------------|
211
+ | 1 | ROOT | All permissions. Also allows setting non-ROOT permissions for others on the same project. |
212
+ | 2 | QUEUE_RULESETS | `JBController.queueRulesetsOf` |
213
+ | 3 | LAUNCH_RULESETS | `JBController.launchRulesetsFor` |
214
+ | 4 | CASH_OUT_TOKENS | `JBMultiTerminal.cashOutTokensOf` |
215
+ | 5 | SEND_PAYOUTS | `JBMultiTerminal.sendPayoutsOf` (only when `ownerMustSendPayouts` is set) |
216
+ | 6 | MIGRATE_TERMINAL | `JBMultiTerminal.migrateBalanceOf` |
217
+ | 7 | SET_PROJECT_URI | `JBController.setUriOf` |
218
+ | 8 | DEPLOY_ERC20 | `JBController.deployERC20For` |
219
+ | 9 | SET_TOKEN | `JBController.setTokenFor` |
220
+ | 10 | MINT_TOKENS | `JBController.mintTokensOf` |
221
+ | 11 | BURN_TOKENS | `JBController.burnTokensOf` |
222
+ | 12 | CLAIM_TOKENS | `JBController.claimTokensFor` |
223
+ | 13 | TRANSFER_CREDITS | `JBController.transferCreditsFrom` |
224
+ | 14 | SET_CONTROLLER | `JBDirectory.setControllerOf` |
225
+ | 15 | SET_TERMINALS | `JBDirectory.setTerminalsOf` |
226
+ | 16 | SET_PRIMARY_TERMINAL | `JBDirectory.setPrimaryTerminalOf` |
227
+ | 17 | USE_ALLOWANCE | `JBMultiTerminal.useAllowanceOf` |
228
+ | 18 | SET_SPLIT_GROUPS | `JBController.setSplitGroupsOf` |
229
+ | 19 | ADD_PRICE_FEED | `JBController.addPriceFeed` |
230
+ | 20 | ADD_ACCOUNTING_CONTEXTS | `JBMultiTerminal.addAccountingContextsFor` |
231
+
232
+ ## Immutable Configuration
233
+
234
+ The following values are set at deploy time and cannot be changed:
235
+
236
+ ### JBController
237
+ - `DIRECTORY` -- the directory contract
238
+ - `FUND_ACCESS_LIMITS` -- the fund access limits contract
239
+ - `PERMISSIONS` -- the permissions contract (from JBPermissioned)
240
+ - `PRICES` -- the prices contract
241
+ - `PROJECTS` -- the projects NFT contract
242
+ - `RULESETS` -- the rulesets contract
243
+ - `SPLITS` -- the splits contract
244
+ - `TOKENS` -- the tokens contract
245
+ - `OMNICHAIN_RULESET_OPERATOR` -- the address allowed to launch/queue rulesets for any project
246
+
247
+ ### JBMultiTerminal
248
+ - `DIRECTORY` -- derived from STORE.DIRECTORY()
249
+ - `FEELESS_ADDRESSES` -- the feeless addresses registry
250
+ - `FEE` -- hardcoded at 25 (2.5% of MAX_FEE=1000)
251
+ - `PERMISSIONS` -- the permissions contract
252
+ - `PERMIT2` -- the Uniswap Permit2 contract
253
+ - `PROJECTS` -- the projects NFT contract
254
+ - `RULESETS` -- derived from STORE.RULESETS()
255
+ - `SPLITS` -- the splits contract
256
+ - `STORE` -- the terminal store contract
257
+ - `TOKENS` -- the tokens contract
258
+ - `_FEE_BENEFICIARY_PROJECT_ID` -- hardcoded as 1
259
+ - `_FEE_HOLDING_SECONDS` -- hardcoded as 2,419,200 (28 days)
260
+
261
+ ### JBDirectory
262
+ - `PERMISSIONS` -- the permissions contract
263
+ - `PROJECTS` -- the projects NFT contract
264
+
265
+ ### JBPrices
266
+ - `DIRECTORY` -- the directory contract
267
+ - `PERMISSIONS` -- the permissions contract
268
+ - `PROJECTS` -- the projects NFT contract
269
+ - Price feeds are immutable once set (cannot be replaced or removed)
270
+
271
+ ### JBTokens
272
+ - `DIRECTORY` -- the directory contract
273
+ - `TOKEN` -- the JBERC20 implementation used for cloning
274
+ - Project token assignments are immutable once set (cannot be changed to a different token)
275
+
276
+ ### JBRulesets
277
+ - `DIRECTORY` -- the directory contract
278
+
279
+ ### JBSplits
280
+ - `DIRECTORY` -- the directory contract
281
+
282
+ ### JBFundAccessLimits
283
+ - `DIRECTORY` -- the directory contract
284
+
285
+ ### JBTerminalStore
286
+ - `DIRECTORY` -- the directory contract
287
+ - `PRICES` -- the prices contract
288
+ - `RULESETS` -- the rulesets contract
289
+
290
+ ### JBPermissions
291
+ - Trusted forwarder for ERC-2771 meta-transactions
292
+
293
+ ## Ruleset Flags That Gate Admin Actions
294
+
295
+ Several admin functions are further gated by boolean flags in the active ruleset's metadata. These flags are set when the ruleset is queued and cannot be changed until a new ruleset takes effect:
296
+
297
+ | Flag | What It Gates |
298
+ |------|--------------|
299
+ | `allowOwnerMinting` | Whether the project owner/operator can call `mintTokensOf`. Terminals and data hooks can always mint regardless. |
300
+ | `allowSetCustomToken` | Whether `setTokenFor` can be called. |
301
+ | `allowTerminalMigration` | Whether `migrateBalanceOf` / `recordTerminalMigration` can proceed. |
302
+ | `allowSetTerminals` | Whether `setTerminalsOf` can be called by non-controller callers. |
303
+ | `allowSetController` | Whether `setControllerOf` can be called (checked via `IJBDirectoryAccessControl`). |
304
+ | `allowAddAccountingContext` | Whether `addAccountingContextsFor` can add new token contexts. |
305
+ | `allowAddPriceFeed` | Whether `addPriceFeed` can add new price feeds. |
306
+ | `ownerMustSendPayouts` | Whether `sendPayoutsOf` requires SEND_PAYOUTS permission (otherwise anyone can call it). |
307
+ | `pausePay` | Whether payments are paused (checked in JBTerminalStore). |
308
+ | `pauseCreditTransfers` | Whether credit transfers are paused. |
309
+ | `holdFees` | Whether fees are held (deferred for 28 days) instead of being processed immediately. |
310
+
311
+ ## Admin Boundaries
312
+
313
+ What admins CANNOT do:
314
+
315
+ - **Project owners cannot access other projects' funds.** All permission checks are scoped to a specific `projectId`. A project owner's permissions only apply to their own project.
316
+ - **Controllers cannot bypass terminal accounting.** JBTerminalStore uses `msg.sender`-scoped balances. A controller cannot directly manipulate a terminal's recorded balances -- only the terminal itself can update its own balance slots.
317
+ - **Terminals cannot mint tokens without controller involvement.** Token minting goes through the controller's `mintTokensOf`, which enforces ruleset rules (reserved percent, `allowOwnerMinting`).
318
+ - **No single admin can change the fee rate.** The fee (2.5%) is hardcoded as a constant in JBMultiTerminal. Changing it requires deploying a new terminal contract.
319
+ - **No admin can change the fee beneficiary.** Project ID 1 is hardcoded as the fee recipient. This cannot be changed.
320
+ - **Price feeds cannot be replaced.** Once a price feed is set for a currency pair (in either direction), it is permanent for that project ID. Recovery requires deploying a new JBPrices contract.
321
+ - **Token assignments are one-way.** Once a project's ERC-20 token is set (via `deployERC20For` or `setTokenFor`), it cannot be changed to a different token.
322
+ - **Locked splits cannot be removed.** Splits with a `lockedUntil` timestamp in the future must be preserved (with the same or extended lock) when updating split groups.
323
+ - **Operators cannot escalate to ROOT via ROOT.** A ROOT operator can set permissions for other operators on the same project but cannot grant ROOT to them or set permissions on the wildcard project ID.
324
+ - **The directory owner cannot set controllers directly.** The directory owner can only manage the `isAllowedToSetFirstController` allowlist. They cannot set or change a project's controller.
325
+ - **Ruleset approval hooks can block changes.** If a ruleset specifies an approval hook, queued rulesets must be approved by that hook before they take effect. A rejected ruleset falls back to the previous approved ruleset's cycling behavior.
326
+ - **Reserved tokens must be distributed before controller migration.** The `migrate` function reverts if `pendingReservedTokenBalanceOf[projectId] > 0`, preventing loss of reserved tokens during migration.
327
+ - **The JBTerminalStore has no admin functions.** It has no owner, no permission checks, and no special roles. Access is controlled entirely by the msg.sender-scoped storage pattern -- callers can only affect their own balance slots.
@@ -0,0 +1,115 @@
1
+ # nana-core-v6 — Architecture
2
+
3
+ ## Purpose
4
+
5
+ Core protocol for Juicebox V6. Provides programmable treasuries with configurable governance, bonding-curve cash outs, split-based payouts, and a compositional hook system.
6
+
7
+ ## Contract Map
8
+
9
+ ```
10
+ src/
11
+ ├── JBMultiTerminal.sol — Multi-token payment terminal (pay, cash out, payouts, fees)
12
+ ├── JBController.sol — Orchestrator (project lifecycle, rulesets, token minting, reserved tokens)
13
+ ├── JBTerminalStore.sol — Bookkeeping (balances, payout limits, surplus, bonding curve math)
14
+ ├── JBRulesets.sol — Ruleset lifecycle (linked-list, weight decay, approval hooks)
15
+ ├── JBDirectory.sol — Routes projects to terminals and controllers
16
+ ├── JBTokens.sol — Dual token system (internal credits + ERC-20)
17
+ ├── JBSplits.sol — Packed split storage with lock enforcement
18
+ ├── JBFundAccessLimits.sol — Payout limits and surplus allowances
19
+ ├── JBPrices.sol — Price feeds with project-specific + default fallback
20
+ ├── JBPermissions.sol — 256-bit packed permission system
21
+ ├── JBProjects.sol — ERC-721 project ownership
22
+ ├── JBERC20.sol — Cloneable ERC20Votes+Permit token
23
+ ├── JBFeelessAddresses.sol — Fee-exempt address registry
24
+ ├── JBDeadline.sol — Approval hook requiring minimum delay
25
+ ├── JBChainlinkV3PriceFeed.sol — Chainlink v3 price feed with staleness check
26
+ ├── JBChainlinkV3SequencerPriceFeed.sol — L2 sequencer-aware price feed
27
+ ├── abstract/
28
+ │ ├── JBPermissioned.sol — Base for permission-checked contracts
29
+ │ └── JBControlled.sol — Base for controller-gated contracts
30
+ └── libraries/
31
+ ├── JBCashOuts.sol — Bonding curve math
32
+ ├── JBFees.sol — Fee calculation (forward/backward)
33
+ ├── JBRulesetMetadataResolver.sol — Bit-packed metadata (256 bits)
34
+ ├── JBMetadataResolver.sol — Variable-length key-value metadata
35
+ ├── JBFixedPointNumber.sol — Decimal adjustment
36
+ ├── JBConstants.sol — Protocol constants
37
+ └── JBSplitGroupIds.sol — Split group ID constants
38
+ ```
39
+
40
+ ## Key Data Flows
41
+
42
+ ### Payment Flow
43
+
44
+ ```
45
+ User -> JBMultiTerminal.pay()
46
+ -> JBTerminalStore.recordPaymentFrom()
47
+ -> Read current ruleset
48
+ -> [Optional] Data hook overrides weight
49
+ -> Calculate token count from weight
50
+ -> Update balance
51
+ -> JBController.mintTokensOf()
52
+ -> Calculate reserved tokens
53
+ -> Mint beneficiary tokens
54
+ -> Accumulate pendingReservedTokenBalanceOf
55
+ -> [Optional] Pay hooks execute
56
+ ```
57
+
58
+ ### Cash Out Flow
59
+
60
+ ```
61
+ Holder -> JBMultiTerminal.cashOutTokensOf()
62
+ -> JBTerminalStore.recordCashOutFor()
63
+ -> Calculate surplus (all terminals, converted via JBPrices)
64
+ -> Get totalSupply (including pending reserved)
65
+ -> [Optional] Data hook overrides parameters
66
+ -> JBCashOuts.cashOutFrom() — bonding curve
67
+ -> Deduct balance
68
+ -> JBController.burnTokensOf()
69
+ -> Transfer reclaimed tokens to beneficiary
70
+ -> [Optional] Cash out hooks execute
71
+ -> Take fees (2.5% to project #1)
72
+ ```
73
+
74
+ ### Payout Flow
75
+
76
+ ```
77
+ Owner -> JBMultiTerminal.sendPayoutsOf()
78
+ -> JBTerminalStore.recordPayoutFor()
79
+ -> Deduct balance, check payout limits
80
+ -> Distribute to splits (JBSplits)
81
+ -> Split to project -> pay project's terminal
82
+ -> Split to address -> direct transfer
83
+ -> Split to hook -> IJBSplitHook.processSplitWith()
84
+ -> Take fees on non-feeless payouts
85
+ ```
86
+
87
+ ## Extension Points
88
+
89
+ | Extension Point | Interface | Called By |
90
+ |----------------|-----------|-----------|
91
+ | Data Hook (pay) | `IJBRulesetDataHook.beforePayRecordedWith` | JBTerminalStore |
92
+ | Data Hook (cashout) | `IJBRulesetDataHook.beforeCashOutRecordedWith` | JBTerminalStore |
93
+ | Pay Hook | `IJBPayHook.afterPayRecordedWith` | JBMultiTerminal |
94
+ | Cash Out Hook | `IJBCashOutHook.afterCashOutRecordedWith` | JBMultiTerminal |
95
+ | Split Hook | `IJBSplitHook.processSplitWith` | JBMultiTerminal |
96
+ | Approval Hook | `IJBRulesetApprovalHook.approvalStatusOf` | JBRulesets |
97
+
98
+ ## Dependencies
99
+
100
+ - `@bananapus/permission-ids-v6` — Permission ID constants
101
+ - `@openzeppelin/contracts` — ERC-721, ERC-20, ERC2771, Clones
102
+ - `@prb/math` — Fixed-point math (mulDiv)
103
+ - `@chainlink/contracts` — Price feed interfaces
104
+ - `@uniswap/permit2` — Permit2 token approvals
105
+
106
+ ## Key Constants
107
+
108
+ - FEE = 25 (2.5%), MAX_FEE = 1000
109
+ - MAX_RESERVED_PERCENT = 10,000 (basis points)
110
+ - MAX_CASH_OUT_TAX_RATE = 10,000
111
+ - MAX_WEIGHT_CUT_PERCENT = 1,000,000,000 (9 decimals)
112
+ - SPLITS_TOTAL_PERCENT = 1,000,000,000
113
+ - NATIVE_TOKEN = 0x000000000000000000000000000000000000EEEe
114
+ - Fee holding: 28 days (2,419,200 seconds)
115
+ - Fee beneficiary: project ID 1
package/RISKS.md ADDED
@@ -0,0 +1,68 @@
1
+ # nana-core-v6 — Risks
2
+
3
+ ## Trust Assumptions
4
+
5
+ ### What You Trust
6
+
7
+ 1. **Shared Infrastructure** — All projects share the same JBMultiTerminal and JBController instances. A bug in these contracts affects every project.
8
+ 2. **Project Owner** — The ERC-721 holder can queue new rulesets, set terminals, configure splits, and delegate permissions. A malicious or compromised owner can fundamentally change project economics.
9
+ 3. **Data Hooks** — If a ruleset specifies a data hook, that hook has absolute control over token minting weights and cash out parameters. A malicious data hook can drain the entire project treasury.
10
+ 4. **Approval Hooks** — Can approve or reject ruleset transitions. A reverting approval hook doesn't freeze the project (try-catch fallback), but a malicious one could allow unexpected transitions.
11
+ 5. **Price Feeds** — Surplus calculations depend on Chainlink price feeds. Stale or manipulated feeds affect cash out values and payout calculations. Staleness causes reverts (DoS), not fund loss.
12
+ 6. **Fee Project (#1)** — 2.5% fees go to project #1. If project #1's terminal is misconfigured, fees are returned to the originating project's balance (not lost).
13
+
14
+ ### What You Do NOT Need to Trust
15
+
16
+ - **Other projects** — Each project's balance is isolated by terminal address in JBTerminalStore
17
+ - **Token holders** — Can only cash out proportional to the bonding curve
18
+ - **Permit2** — Optional; projects work without it
19
+
20
+ ## Known Risks
21
+
22
+ ### By Design
23
+
24
+ | Risk | Description | Mitigation |
25
+ |------|-------------|------------|
26
+ | Data hook omnipotence | Data hooks override bonding curve parameters | Only use audited, trusted data hooks |
27
+ | Last-holder advantage | Last token holder redeems remaining surplus at 1:1 | Bonding curve math; inherent to the design |
28
+ | Pending reserved inflation | Pending reserved tokens dilute cash out values | Call `sendReservedTokensToSplitsOf` regularly |
29
+ | No reentrancy guard | Protocol relies on CEI ordering, not mutex | State updates before all external calls |
30
+ | Weight cache requirement | Projects with >20k cycles need progressive cache updates | Anyone can call `updateRulesetWeightCache` |
31
+
32
+ ### Operational
33
+
34
+ | Risk | Description | Mitigation |
35
+ |------|-------------|------------|
36
+ | Price feed DoS | Stale/reverting price feed blocks multi-currency operations | Monitor feed health; single-currency projects unaffected |
37
+ | Split gas exhaustion | Very large split arrays (100+) may exceed block gas | Keep split count reasonable (<50) |
38
+ | Held fee growth | Held fees array grows without cleanup | `_nextHeldFeeIndexOf` pointer skips processed entries |
39
+
40
+ ## Reentrancy Analysis
41
+
42
+ No `ReentrancyGuard`. Relies on state ordering (checks-effects-interactions):
43
+
44
+ | Function | State Updated Before External Call | Risk |
45
+ |----------|-----------------------------------|------|
46
+ | `_cashOutTokensOf` | Store balance deducted, tokens burned BEFORE transfer | LOW |
47
+ | `_pay` | Store balance added, tokens minted BEFORE pay hooks | LOW |
48
+ | `executePayout` | Payout limit recorded BEFORE split hook calls | LOW |
49
+ | `processHeldFeesOf` | Index updated BEFORE fee processing | LOW |
50
+ | `_sendReservedTokensToSplitsOf` | Pending balance zeroed BEFORE minting | LOW |
51
+
52
+ **Key defense:** `JBTerminalStore_InadequateTerminalStoreBalance` revert prevents extracting more than available balance regardless of reentrancy.
53
+
54
+ ## Permission Security
55
+
56
+ - ROOT (ID 1) grants all permissions but cannot be set for wildcard `projectId = 0`
57
+ - ROOT operators cannot grant ROOT to other addresses
58
+ - Permission 0 is reserved and cannot be set
59
+ - All permission checks support ERC-2771 meta-transactions
60
+
61
+ ## Proven Invariants
62
+
63
+ 1. No flash-loan profit (12 attack vectors tested)
64
+ 2. Terminal balance >= sum of recorded project balances
65
+ 3. Total inflows >= total outflows
66
+ 4. Fee project balance monotonically increases
67
+ 5. Token supply = creditSupply + erc20.totalSupply()
68
+ 6. Current ruleset always exists after launch
package/SKILLS.md CHANGED
@@ -246,6 +246,11 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
246
246
  - `IJBDirectoryAccessControl` has `setControllerAllowed()` and `setTerminalsAllowed()` -- NOT `setControllerAllowedFor()`
247
247
  - Price feeds are immutable once set in `JBPrices` -- they cannot be replaced or removed
248
248
  - `JBFundAccessLimits` requires payout limits and surplus allowances to be in strictly increasing currency order to prevent duplicates
249
+ - **Empty `fundAccessLimitGroups` = zero payouts, NOT unlimited.** If a ruleset's `fundAccessLimitGroups` array is empty (or has no entry for the terminal/token), `payoutLimitsOf()` returns an empty array → cumulative limit is 0 → `sendPayoutsOf()` reverts on any amount. To allow unlimited payouts, explicitly set a payout limit with `amount: type(uint224).max`.
250
+ - **`groupId` (uint256) vs `currency` (uint32) are different types for the same address.** `JBSplitGroup.groupId` is `uint256(uint160(tokenAddress))` while `JBAccountingContext.currency` is `uint32(uint160(tokenAddress))`. These truncate differently — only `NATIVE_TOKEN` (0x000000000000000000000000000000000000EEEe) matches by coincidence. Don't confuse them.
251
+ - **`JBAccountingContext.currency` is NOT `baseCurrency` — by design.** `baseCurrency` in ruleset metadata uses abstract real-world values (1 = ETH, 2 = USD) so rulesets are portable across chains — `baseCurrency=2` means "issue X tokens per USD" whether on Ethereum, Base, or Arbitrum. `JBAccountingContext.currency` uses token-derived values (`uint32(uint160(tokenAddress))`) because terminals track specific tokens at specific addresses — e.g. NATIVE_TOKEN = 61166, USDC on Ethereum = 909516616, USDC on Base = 3169378579. `JBPrices` mediates between the two: it converts token-derived currencies to/from abstract currencies (e.g. USDC token → USD concept, NATIVE_TOKEN → ETH concept) so that payout limits denominated in USD work correctly regardless of which token the terminal holds. The separation is what makes cross-chain consistency possible: same ruleset, different terminal accounting per chain.
252
+ - **Don't queue multiple identical rulesets.** A ruleset with a `duration` automatically cycles — no need to queue copies. Queue multiple rulesets only when configuration actually changes between periods (e.g. different weight, splits, or limits).
253
+ - **`NATIVE_TOKEN` represents a different token on each chain.** `NATIVE_TOKEN` (`0x000000000000000000000000000000000000EEEe`) is the token received via `msg.value` — ETH on Ethereum/Base/Optimism/Arbitrum, CELO on Celo, etc. Its currency is `uint32(uint160(NATIVE_TOKEN))` = 61166. A `JBMatchingPriceFeed` (returns 1:1) is deployed for `ETH:NATIVE_TOKEN` on ETH-native chains so that `baseCurrency=ETH` resolves correctly to the native token. On non-ETH-native chains, a different price feed would be needed.
249
254
 
250
255
  ## Example Integration
251
256
 
package/STYLE_GUIDE.md ADDED
@@ -0,0 +1,465 @@
1
+ # Style Guide
2
+
3
+ How we write Solidity and organize repos across the Juicebox V6 ecosystem. `nana-core-v6` is the gold standard — when in doubt, match what it does.
4
+
5
+ ## File Organization
6
+
7
+ ```
8
+ src/
9
+ ├── Contract.sol # Main contracts in root
10
+ ├── abstract/ # Base contracts (JBPermissioned, JBControlled)
11
+ ├── enums/ # One enum per file
12
+ ├── interfaces/ # One interface per file, prefixed with I
13
+ ├── libraries/ # Pure/view logic, prefixed with JB
14
+ ├── periphery/ # Utility contracts (deadlines, price feeds)
15
+ └── structs/ # One struct per file, prefixed with JB
16
+ ```
17
+
18
+ One contract/interface/struct/enum per file. Name the file after the type it contains.
19
+
20
+ **Structs, enums, libraries, and interfaces always go in their subdirectories** (`src/structs/`, `src/enums/`, `src/libraries/`, `src/interfaces/`) — never inline in contract files or placed in `src/` root. This keeps type definitions discoverable and import paths consistent across repos.
21
+
22
+ ## Pragma Versions
23
+
24
+ ```solidity
25
+ // Contracts — pin to exact version
26
+ pragma solidity 0.8.26;
27
+
28
+ // Interfaces, structs, enums — caret for forward compatibility
29
+ pragma solidity ^0.8.0;
30
+
31
+ // Libraries — caret, may use newer features
32
+ pragma solidity ^0.8.17;
33
+ ```
34
+
35
+ ## Imports
36
+
37
+ Named imports only. Grouped by source, alphabetized within each group:
38
+
39
+ ```solidity
40
+ // External packages (alphabetized)
41
+ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
42
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
43
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
44
+ import {mulDiv} from "@prb/math/src/Common.sol";
45
+
46
+ // Local: abstract contracts
47
+ import {JBPermissioned} from "./abstract/JBPermissioned.sol";
48
+
49
+ // Local: interfaces (alphabetized)
50
+ import {IJBController} from "./interfaces/IJBController.sol";
51
+ import {IJBDirectory} from "./interfaces/IJBDirectory.sol";
52
+ import {IJBMultiTerminal} from "./interfaces/IJBMultiTerminal.sol";
53
+
54
+ // Local: libraries (alphabetized)
55
+ import {JBConstants} from "./libraries/JBConstants.sol";
56
+ import {JBFees} from "./libraries/JBFees.sol";
57
+
58
+ // Local: structs (alphabetized)
59
+ import {JBAccountingContext} from "./structs/JBAccountingContext.sol";
60
+ import {JBSplit} from "./structs/JBSplit.sol";
61
+ ```
62
+
63
+ ## Contract Structure
64
+
65
+ Section banners divide the contract into a fixed ordering. Every contract with 50+ lines uses these banners:
66
+
67
+ ```solidity
68
+ /// @notice One-line description.
69
+ contract JBExample is JBPermissioned, IJBExample {
70
+ // A library that does X.
71
+ using SomeLib for SomeType;
72
+
73
+ //*********************************************************************//
74
+ // --------------------------- custom errors ------------------------- //
75
+ //*********************************************************************//
76
+
77
+ error JBExample_SomethingFailed(uint256 amount);
78
+
79
+ //*********************************************************************//
80
+ // ------------------------- public constants ------------------------ //
81
+ //*********************************************************************//
82
+
83
+ uint256 public constant override FEE = 25;
84
+
85
+ //*********************************************************************//
86
+ // ----------------------- internal constants ------------------------ //
87
+ //*********************************************************************//
88
+
89
+ uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
90
+
91
+ //*********************************************************************//
92
+ // --------------- public immutable stored properties ---------------- //
93
+ //*********************************************************************//
94
+
95
+ IJBDirectory public immutable override DIRECTORY;
96
+
97
+ //*********************************************************************//
98
+ // --------------------- public stored properties -------------------- //
99
+ //*********************************************************************//
100
+
101
+ //*********************************************************************//
102
+ // -------------------- internal stored properties ------------------- //
103
+ //*********************************************************************//
104
+
105
+ //*********************************************************************//
106
+ // -------------------------- constructor ---------------------------- //
107
+ //*********************************************************************//
108
+
109
+ //*********************************************************************//
110
+ // ---------------------- receive / fallback ------------------------- //
111
+ //*********************************************************************//
112
+
113
+ //*********************************************************************//
114
+ // --------------------------- modifiers ----------------------------- //
115
+ //*********************************************************************//
116
+
117
+ //*********************************************************************//
118
+ // ---------------------- external transactions ---------------------- //
119
+ //*********************************************************************//
120
+
121
+ //*********************************************************************//
122
+ // ----------------------- external views ---------------------------- //
123
+ //*********************************************************************//
124
+
125
+ //*********************************************************************//
126
+ // ----------------------- public transactions ----------------------- //
127
+ //*********************************************************************//
128
+
129
+ //*********************************************************************//
130
+ // ----------------------- internal helpers -------------------------- //
131
+ //*********************************************************************//
132
+
133
+ //*********************************************************************//
134
+ // ----------------------- internal views ---------------------------- //
135
+ //*********************************************************************//
136
+
137
+ //*********************************************************************//
138
+ // ----------------------- private helpers --------------------------- //
139
+ //*********************************************************************//
140
+ }
141
+ ```
142
+
143
+ **Section order:**
144
+ 1. `using` declarations
145
+ 2. Custom errors
146
+ 3. Public constants
147
+ 4. Internal constants
148
+ 5. Public immutable stored properties
149
+ 6. Internal immutable stored properties
150
+ 7. Public stored properties
151
+ 8. Internal stored properties
152
+ 9. Constructor
153
+ 10. `receive` / `fallback`
154
+ 11. Modifiers
155
+ 12. External transactions
156
+ 13. External views
157
+ 14. Public transactions
158
+ 15. Internal helpers
159
+ 16. Internal views
160
+ 17. Private helpers
161
+
162
+ Functions are alphabetized within each section.
163
+
164
+ **Events:** Events are declared in interfaces only, never in implementation contracts. Implementations inherit events from their interface and emit them unqualified. This keeps the ABI definition in one place and allows tests to use interface-qualified event expectations (e.g., `emit IJBController.LaunchProject(...)`).
165
+
166
+ ## Interface Structure
167
+
168
+ ```solidity
169
+ /// @notice One-line description.
170
+ interface IJBExample is IJBBase {
171
+ // Events (with full NatSpec)
172
+
173
+ /// @notice Emitted when X happens.
174
+ /// @param projectId The ID of the project.
175
+ /// @param amount The amount transferred.
176
+ event SomethingHappened(uint256 indexed projectId, uint256 amount);
177
+
178
+ // Views (alphabetized)
179
+
180
+ /// @notice The directory of terminals and controllers.
181
+ function DIRECTORY() external view returns (IJBDirectory);
182
+
183
+ // State-changing functions (alphabetized)
184
+
185
+ /// @notice Does the thing.
186
+ /// @param projectId The ID of the project.
187
+ /// @return result The result.
188
+ function doThing(uint256 projectId) external returns (uint256 result);
189
+ }
190
+ ```
191
+
192
+ **Rules:**
193
+ - Events first, then views, then state-changing functions
194
+ - No custom errors in interfaces — errors belong in the implementing contract
195
+ - Full NatSpec on every event, function, and parameter
196
+ - Alphabetized within each group
197
+
198
+ ## Naming
199
+
200
+ | Thing | Convention | Example |
201
+ |-------|-----------|---------|
202
+ | Contract | PascalCase | `JBMultiTerminal` |
203
+ | Interface | `I` + PascalCase | `IJBMultiTerminal` |
204
+ | Library | PascalCase | `JBCashOuts` |
205
+ | Struct | PascalCase | `JBRulesetConfig` |
206
+ | Enum | PascalCase | `JBApprovalStatus` |
207
+ | Enum value | PascalCase | `ApprovalExpected` |
208
+ | Error | `ContractName_ErrorName` | `JBMultiTerminal_FeeTerminalNotFound` |
209
+ | Public constant | `ALL_CAPS` | `FEE`, `MAX_FEE` |
210
+ | Internal constant | `_ALL_CAPS` | `_FEE_HOLDING_SECONDS` |
211
+ | Public immutable | `ALL_CAPS` | `DIRECTORY`, `PERMISSIONS` |
212
+ | Public/external function | `camelCase` | `cashOutTokensOf` |
213
+ | Internal/private function | `_camelCase` | `_processFee` |
214
+ | Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
215
+ | Function parameter | `camelCase` | `projectId`, `cashOutCount` |
216
+
217
+ ## NatSpec
218
+
219
+ **Contracts:**
220
+ ```solidity
221
+ /// @notice One-line description of what the contract does.
222
+ contract JBExample is IJBExample {
223
+ ```
224
+
225
+ **Functions:**
226
+ ```solidity
227
+ /// @notice Records funds being added to a project's balance.
228
+ /// @param projectId The ID of the project which funds are being added to.
229
+ /// @param token The token being added.
230
+ /// @param amount The amount added, as a fixed point number with the same decimals as the terminal.
231
+ /// @return surplus The new surplus after adding.
232
+ function recordAddedBalanceFor(
233
+ uint256 projectId,
234
+ address token,
235
+ uint256 amount
236
+ ) external override returns (uint256 surplus) {
237
+ ```
238
+
239
+ **Structs:**
240
+ ```solidity
241
+ /// @custom:member duration The number of seconds the ruleset lasts for. 0 means it never expires.
242
+ /// @custom:member weight How many tokens to mint per unit paid (18 decimals).
243
+ /// @custom:member weightCutPercent How much weight decays each cycle (9 decimals).
244
+ struct JBRulesetConfig {
245
+ uint32 duration;
246
+ uint112 weight;
247
+ uint32 weightCutPercent;
248
+ }
249
+ ```
250
+
251
+ **Mappings:**
252
+ ```solidity
253
+ /// @notice Context describing how a token is accounted for by a project.
254
+ /// @custom:param projectId The ID of the project.
255
+ /// @custom:param token The address of the token.
256
+ mapping(uint256 projectId => mapping(address token => JBAccountingContext)) internal _accountingContextForTokenOf;
257
+ ```
258
+
259
+ ## Numbers
260
+
261
+ Use underscores for thousands separators:
262
+
263
+ ```solidity
264
+ uint256 internal constant _FEE_HOLDING_SECONDS = 2_419_200; // 28 days
265
+ uint32 public constant MAX_WEIGHT_CUT_PERCENT = 1_000_000_000;
266
+ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
267
+ ```
268
+
269
+ ## Function Calls
270
+
271
+ Use named parameters for readability when calling functions with 3+ arguments:
272
+
273
+ ```solidity
274
+ PERMISSIONS.hasPermission({
275
+ operator: sender,
276
+ account: account,
277
+ projectId: projectId,
278
+ permissionId: permissionId,
279
+ includeRoot: true,
280
+ includeWildcardProjectId: true
281
+ });
282
+ ```
283
+
284
+ ## Multiline Signatures
285
+
286
+ ```solidity
287
+ function recordCashOutFor(
288
+ address holder,
289
+ uint256 projectId,
290
+ uint256 cashOutCount,
291
+ JBAccountingContext calldata accountingContext
292
+ )
293
+ external
294
+ override
295
+ returns (
296
+ JBRuleset memory ruleset,
297
+ uint256 reclaimAmount,
298
+ JBCashOutHookSpecification[] memory hookSpecifications
299
+ )
300
+ {
301
+ ```
302
+
303
+ Modifiers and return types go on their own indented lines.
304
+
305
+ ## Error Handling
306
+
307
+ - Validate inputs with explicit `revert` + custom error
308
+ - Use `try-catch` only for external calls to untrusted contracts (hooks, fee processing)
309
+ - Always include relevant context in error parameters
310
+
311
+ ```solidity
312
+ // Direct validation
313
+ if (amount > limit) revert JBTerminalStore_InadequateControllerPayoutLimit(amount, limit);
314
+
315
+ // External call to untrusted hook
316
+ try hook.afterPayRecordedWith(context) {} catch (bytes memory reason) {
317
+ emit HookAfterPayReverted(hook, context, reason, _msgSender());
318
+ }
319
+ ```
320
+
321
+ ---
322
+
323
+ ## DevOps
324
+
325
+ ### foundry.toml
326
+
327
+ Standard config for `@bananapus/core-v6`:
328
+
329
+ ```toml
330
+ [profile.default]
331
+ solc = '0.8.26'
332
+ evm_version = 'cancun'
333
+ optimizer_runs = 200
334
+ libs = ["node_modules", "lib"]
335
+ fs_permissions = [{ access = "read-write", path = "./"}]
336
+
337
+ [profile.ci_sizes]
338
+ optimizer_runs = 200
339
+
340
+ [fuzz]
341
+ runs = 4096
342
+
343
+ [invariant]
344
+ runs = 1024
345
+ depth = 100
346
+ fail_on_revert = false
347
+
348
+ [fmt]
349
+ number_underscore = "thousands"
350
+ multiline_func_header = "all"
351
+ wrap_comments = true
352
+ ```
353
+
354
+ This is the standard config with no deviations.
355
+
356
+ ### CI Workflows
357
+
358
+ Every repo has at minimum `test.yml` and `lint.yml`:
359
+
360
+ **test.yml:**
361
+ ```yaml
362
+ name: test
363
+ on:
364
+ pull_request:
365
+ branches: [main]
366
+ push:
367
+ branches: [main]
368
+ jobs:
369
+ forge-test:
370
+ runs-on: ubuntu-latest
371
+ steps:
372
+ - uses: actions/checkout@v4
373
+ with:
374
+ submodules: recursive
375
+ - uses: actions/setup-node@v4
376
+ with:
377
+ node-version: 22.4.x
378
+ - name: Install npm dependencies
379
+ run: npm install --omit=dev
380
+ - name: Install Foundry
381
+ uses: foundry-rs/foundry-toolchain@v1
382
+ - name: Run tests
383
+ run: forge test --fail-fast --summary --detailed --skip "*/script/**"
384
+ - name: Check contract sizes
385
+ run: FOUNDRY_PROFILE=ci_sizes forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
386
+ ```
387
+
388
+ **lint.yml:**
389
+ ```yaml
390
+ name: lint
391
+ on:
392
+ pull_request:
393
+ branches: [main]
394
+ push:
395
+ branches: [main]
396
+ jobs:
397
+ forge-fmt:
398
+ runs-on: ubuntu-latest
399
+ steps:
400
+ - uses: actions/checkout@v4
401
+ - name: Install Foundry
402
+ uses: foundry-rs/foundry-toolchain@v1
403
+ - name: Check formatting
404
+ run: forge fmt --check
405
+ ```
406
+
407
+ ### package.json
408
+
409
+ ```json
410
+ {
411
+ "name": "@bananapus/core-v6",
412
+ "version": "x.x.x",
413
+ "license": "MIT",
414
+ "repository": { "type": "git", "url": "git+https://github.com/Bananapus/nana-core-v6.git" },
415
+ "engines": { "node": ">=20.0.0" },
416
+ "scripts": {
417
+ "test": "forge test",
418
+ "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary"
419
+ },
420
+ "dependencies": { ... },
421
+ "devDependencies": {
422
+ "@sphinx-labs/plugins": "^0.33.2"
423
+ }
424
+ }
425
+ ```
426
+
427
+ **Scoping:** `@bananapus/` for Bananapus repos, `@rev-net/` for revnet, `@croptop/` for croptop, `@bannynet/` for banny, `@ballkidz/` for defifa.
428
+
429
+ ### remappings.txt
430
+
431
+ Every repo has a `remappings.txt`. Minimal content:
432
+
433
+ ```
434
+ @sphinx-labs/contracts/=lib/sphinx/packages/contracts/contracts/foundry
435
+ ```
436
+
437
+ Additional mappings as needed for repo-specific dependencies.
438
+
439
+ ### Formatting
440
+
441
+ Run `forge fmt` before committing. The `[fmt]` config in `foundry.toml` enforces:
442
+ - Thousands separators on numbers (`1_000_000`)
443
+ - Multiline function headers when multiple parameters
444
+ - Wrapped comments at reasonable width
445
+
446
+ CI checks formatting via `forge fmt --check`.
447
+
448
+ ### Branching
449
+
450
+ - `main` is the primary branch
451
+ - Feature branches for PRs
452
+ - All PRs trigger test + lint workflows
453
+ - Submodule checkout with `--recursive` in CI
454
+
455
+ ### Dependencies
456
+
457
+ - Solidity dependencies via npm (`node_modules/`)
458
+ - `forge-std` as a git submodule in `lib/`
459
+ - Sphinx plugins as a devDependency
460
+ - Cross-repo references use `file:../sibling-repo` in local development
461
+ - Published versions use semver ranges (`^0.0.x`) for npm
462
+
463
+ ### Contract Size Checks
464
+
465
+ CI runs `FOUNDRY_PROFILE=ci_sizes forge build --sizes` to catch contracts approaching the 24KB limit. The `ci_sizes` profile uses `optimizer_runs = 200` for realistic size measurement even when the default profile has different optimizer settings.
package/foundry.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [profile.default]
2
2
  solc = '0.8.26'
3
- evm_version = 'paris'
3
+ evm_version = 'cancun'
4
4
  optimizer_runs = 200
5
5
  libs = ["node_modules", "lib"]
6
6
  fs_permissions = [{ access = "read-write", path = "./"}]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
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.5",
29
+ "@bananapus/permission-ids-v6": "^0.0.6",
30
30
  "@chainlink/contracts": "^1.3.0",
31
31
  "@openzeppelin/contracts": "^5.2.0",
32
32
  "@prb/math": "^4.1.0",
@@ -198,8 +198,8 @@ contract BondingCurveProperties is Test {
198
198
  if (firstResult + secondResult > singleResult) {
199
199
  // The excess should be tiny relative to the result
200
200
  uint256 excess = (firstResult + secondResult) - singleResult;
201
- // Allow up to 1 basis point (0.01%) rounding tolerance
202
- assert(excess * 10_000 <= singleResult);
201
+ // Allow up to 2 wei absolute (for small values) or 0.01% relative
202
+ assert(excess <= 2 || excess * 10_000 <= singleResult);
203
203
  }
204
204
  }
205
205
 
@@ -229,10 +229,13 @@ contract BondingCurveProperties is Test {
229
229
  uint256 secondResult = JBCashOuts.cashOutFrom(remainingSurplus, b, remainingSupply, cashOutTaxRate);
230
230
 
231
231
  // NOTE: Strict subadditivity violated due to mulDiv rounding.
232
- // Verify the weaker property: excess bounded by rounding tolerance (< 0.01%).
232
+ // Verify the weaker property: excess bounded by rounding tolerance (<=2 wei or < 0.01%).
233
233
  if (firstResult + secondResult > singleResult) {
234
234
  uint256 excess = (firstResult + secondResult) - singleResult;
235
- assertLe(excess * 10_000, singleResult, "No-arbitrage: rounding excess should be < 0.01%");
235
+ assertTrue(
236
+ excess <= 2 || excess * 10_000 <= singleResult,
237
+ "No-arbitrage: rounding excess should be <= 2 wei or < 0.01%"
238
+ );
236
239
  }
237
240
  }
238
241