@bananapus/core-v6 0.0.5 → 0.0.6
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/README.md +93 -33
- package/SKILLS.md +166 -30
- package/package.json +1 -1
- package/src/libraries/JBMetadataResolver.sol +7 -7
- package/test/FlashLoanAttacks.t.sol +8 -13
- package/test/PermissionsInvariant.t.sol +403 -0
package/README.md
CHANGED
|
@@ -8,77 +8,127 @@ For full documentation, see [docs.juicebox.money](https://docs.juicebox.money/).
|
|
|
8
8
|
|
|
9
9
|
Juicebox projects have two main entry points:
|
|
10
10
|
|
|
11
|
-
- **Terminals** handle inflows and outflows of funds
|
|
11
|
+
- **Terminals** handle inflows and outflows of funds -- payments, cash outs, payouts, and surplus allowance usage. Each project can use multiple terminals, and a single terminal can serve many projects. `JBMultiTerminal` is the standard implementation.
|
|
12
12
|
- **Controllers** manage rulesets and tokens. `JBController` is the standard implementation that coordinates ruleset queuing, token minting/burning, splits, and fund access limits.
|
|
13
13
|
|
|
14
14
|
`JBDirectory` maps each project to its controller and terminals.
|
|
15
15
|
|
|
16
16
|
### Rulesets
|
|
17
17
|
|
|
18
|
-
A project's behavior is governed by a queue of **rulesets**. Each ruleset defines the rules that apply for a specific duration: payment weight (tokens minted per unit paid), cash out rate, reserved
|
|
18
|
+
A project's behavior is governed by a queue of **rulesets**. Each ruleset defines the rules that apply for a specific duration: payment weight (tokens minted per unit paid), cash out tax rate, reserved percent, payout limits, approval hooks, and more. When a ruleset ends, the next one in the queue takes effect. If the queue is empty, the current ruleset keeps cycling with weight decay applied each cycle. Rulesets give project creators the ability to evolve their project's rules while offering supporters contractual guarantees about the future.
|
|
19
|
+
|
|
20
|
+
Key ruleset behaviors:
|
|
21
|
+
- **Weight** determines token issuance per unit paid. A weight of 1 means "inherit decayed weight from the previous ruleset". A weight of 0 means "no issuance".
|
|
22
|
+
- **Weight decay** is controlled by `weightCutPercent`. Each cycle, the weight is reduced by this percent (9-decimal precision out of `1_000_000_000`).
|
|
23
|
+
- **Duration of 0** means the ruleset never expires and must be explicitly replaced by a new queued ruleset (which takes effect immediately).
|
|
24
|
+
- **Approval hooks** can gate whether queued rulesets take effect. For example, `JBDeadline` requires rulesets to be queued a minimum number of seconds before the current ruleset ends.
|
|
19
25
|
|
|
20
26
|
### Fund Distribution
|
|
21
27
|
|
|
22
|
-
Funds can be accessed through **payouts** (distributed to splits within payout limits, resetting each ruleset) or **surplus allowance** (discretionary withdrawal of surplus funds, does not reset). Funds beyond payout limits are surplus
|
|
28
|
+
Funds can be accessed through **payouts** (distributed to splits within payout limits, resetting each ruleset cycle) or **surplus allowance** (discretionary withdrawal of surplus funds, does not reset each cycle). Funds beyond payout limits are surplus -- available for cash outs if the project's cash out tax rate allows it.
|
|
29
|
+
|
|
30
|
+
- **Payout limits** are denominated in configurable currencies and can be set per terminal/token. Multiple limits in different currencies can be active simultaneously.
|
|
31
|
+
- **Surplus allowances** allow project owners to withdraw surplus funds up to a configured amount, also denominated in configurable currencies.
|
|
23
32
|
|
|
24
33
|
### Payments, Tokens, and Cash Outs
|
|
25
34
|
|
|
26
|
-
Payments mint credits (or ERC-20 tokens if
|
|
35
|
+
Payments mint credits (or ERC-20 tokens if an ERC-20 has been deployed for the project) for the payer based on the current ruleset's weight. The number of tokens minted can be influenced by a data hook.
|
|
36
|
+
|
|
37
|
+
Credits and tokens can be **cashed out** to reclaim surplus funds along a bonding curve determined by the cash out tax rate:
|
|
38
|
+
- A **0% tax rate** gives proportional (1:1) redemption of surplus.
|
|
39
|
+
- A **100% tax rate** means nothing can be reclaimed (all surplus is locked).
|
|
40
|
+
- Tax rates between 0% and 100% create a bonding curve that incentivizes holding -- later cashers-out get a better rate per token.
|
|
41
|
+
|
|
42
|
+
### Reserved Tokens
|
|
43
|
+
|
|
44
|
+
Each ruleset can define a `reservedPercent` (0-10,000 basis points). When tokens are minted from payments, this percentage is set aside. Reserved tokens accumulate in `pendingReservedTokenBalanceOf` and are distributed to the reserved token split group when `sendReservedTokensToSplitsOf` is called.
|
|
27
45
|
|
|
28
46
|
### Permissions
|
|
29
47
|
|
|
30
|
-
`JBPermissions` lets addresses delegate specific capabilities to operators, scoped by project ID. Each permission ID grants access to specific functions
|
|
48
|
+
`JBPermissions` lets addresses delegate specific capabilities to operators, scoped by project ID. Each permission ID grants access to specific functions. See [`JBPermissionIds`](https://github.com/Bananapus/nana-permission-ids-v6/blob/main/src/JBPermissionIds.sol) for the full list.
|
|
49
|
+
|
|
50
|
+
- Permission ID `255` is `ROOT` and grants all permissions for the scoped project.
|
|
51
|
+
- Project ID `0` is a wildcard, granting permissions across all projects (cannot be combined with `ROOT` for safety).
|
|
52
|
+
- ROOT operators can set non-ROOT permissions for other operators, but cannot grant ROOT or set wildcard-project permissions.
|
|
31
53
|
|
|
32
54
|
### Hooks
|
|
33
55
|
|
|
34
56
|
Hooks are customizable contracts that plug into protocol flows:
|
|
35
57
|
|
|
36
|
-
- **Approval hooks**
|
|
37
|
-
- **Data hooks**
|
|
38
|
-
- **Pay hooks**
|
|
39
|
-
- **Cash out hooks**
|
|
40
|
-
- **Split hooks**
|
|
58
|
+
- **Approval hooks** -- Gate whether the next queued ruleset can take effect (e.g., `JBDeadline` enforces a minimum queue time).
|
|
59
|
+
- **Data hooks** -- Override payment/cash-out weight, cash out tax rate, token counts, and specify pay/cash-out hooks to call. Data hooks can also grant `hasMintPermissionFor` to allow addresses to mint tokens on demand.
|
|
60
|
+
- **Pay hooks** -- Custom logic triggered after a payment is recorded (e.g., `JB721TiersHook` mints NFTs). Receive tokens and `JBAfterPayRecordedContext`.
|
|
61
|
+
- **Cash out hooks** -- Custom logic triggered after a cash out is recorded. Receive tokens and `JBAfterCashOutRecordedContext`.
|
|
62
|
+
- **Split hooks** -- Custom logic triggered when a payout or reserved token distribution is routed to a split. Receive tokens and `JBSplitHookContext`.
|
|
41
63
|
|
|
42
64
|
### Fees
|
|
43
65
|
|
|
44
|
-
`JBMultiTerminal` charges a 2.5% fee
|
|
66
|
+
`JBMultiTerminal` charges a 2.5% fee (`FEE = 25` out of `MAX_FEE = 1000`) on:
|
|
67
|
+
- Payouts to external addresses (not to other Juicebox projects).
|
|
68
|
+
- Surplus allowance usage.
|
|
69
|
+
- Cash outs when the cash out tax rate is below 100%.
|
|
70
|
+
|
|
71
|
+
Fees are paid to **project #1** (the fee beneficiary project, minted in the `JBProjects` constructor). Addresses on the `JBFeelessAddresses` allowlist are exempt from fees.
|
|
72
|
+
|
|
73
|
+
When a ruleset has `holdFees` enabled, fees are held for 28 days before being processed. During this period, if funds are returned to the project via `addToBalanceOf`, held fees can be unlocked and returned.
|
|
74
|
+
|
|
75
|
+
### Meta-Transactions
|
|
76
|
+
|
|
77
|
+
`JBController`, `JBMultiTerminal`, `JBProjects`, `JBPrices`, and `JBPermissions` support ERC-2771 meta-transactions through a trusted forwarder. This allows gasless interactions where a relayer submits transactions on behalf of users.
|
|
78
|
+
|
|
79
|
+
### Permit2
|
|
80
|
+
|
|
81
|
+
`JBMultiTerminal` integrates with Uniswap's [Permit2](https://github.com/Uniswap/permit2) for gas-efficient ERC-20 token approvals. Payers can include a `JBSingleAllowance` in the payment metadata to authorize token transfers without a separate approval transaction.
|
|
82
|
+
|
|
83
|
+
### Controller Migration
|
|
84
|
+
|
|
85
|
+
Projects can migrate between controllers using the `IJBMigratable` interface. The migration lifecycle calls `beforeReceiveMigrationFrom` on the new controller, then `migrate` on the old controller (while the directory still points to it), then updates the directory, and finally calls `afterReceiveMigrationFrom`. Terminal migration is also supported via `migrateBalanceOf`.
|
|
45
86
|
|
|
46
87
|
## Architecture
|
|
47
88
|
|
|
48
89
|
Juicebox V6 separates concerns across specialized contracts that coordinate through a central directory. Projects are represented as ERC-721 NFTs. Each project configures rulesets that dictate how payments, payouts, cash outs, and token minting behave over time.
|
|
49
90
|
|
|
91
|
+
All contracts use Solidity `0.8.26`.
|
|
92
|
+
|
|
50
93
|
### Core Contracts
|
|
51
94
|
|
|
52
95
|
| Contract | Description |
|
|
53
96
|
|----------|-------------|
|
|
54
|
-
| `JBProjects` | ERC-721 registry of projects. Minting an NFT creates a project. |
|
|
55
|
-
| `JBPermissions` | Bitmap-based permission system. Accounts grant operators specific permissions scoped to project IDs. |
|
|
56
|
-
| `JBDirectory` | Maps each project to its controller and terminals. Entry point for looking up where to interact with a project. |
|
|
57
|
-
| `JBController` | Coordinates rulesets, tokens, splits, and fund access limits. Entry point for launching projects,
|
|
58
|
-
| `JBMultiTerminal` | Accepts payments (native ETH and ERC-20s), processes cash outs, distributes payouts,
|
|
59
|
-
| `JBTerminalStore` | Bookkeeping engine for all terminal inflows and outflows. Tracks balances, enforces payout limits and surplus allowances,
|
|
97
|
+
| `JBProjects` | ERC-721 registry of projects. Minting an NFT creates a project. Optionally mints project #1 to a fee beneficiary owner. |
|
|
98
|
+
| `JBPermissions` | Bitmap-based permission system. Accounts grant operators specific permissions scoped to project IDs. Supports ROOT (255) for all-permissions and wildcard project ID (0). |
|
|
99
|
+
| `JBDirectory` | Maps each project to its controller and terminals. Entry point for looking up where to interact with a project. Manages an allowlist of addresses permitted to set a project's first controller. |
|
|
100
|
+
| `JBController` | Coordinates rulesets, tokens, splits, and fund access limits. Entry point for launching projects, queuing rulesets, minting/burning tokens, deploying ERC-20s, sending reserved tokens, setting project URIs, adding price feeds, and transferring credits. |
|
|
101
|
+
| `JBMultiTerminal` | Accepts payments (native ETH and ERC-20s), processes cash outs, distributes payouts, manages surplus allowances, and handles fees. Integrates with Permit2 for ERC-20 approvals. |
|
|
102
|
+
| `JBTerminalStore` | Bookkeeping engine for all terminal inflows and outflows. Tracks balances, enforces payout limits and surplus allowances, computes cash out reclaim amounts via a bonding curve, and integrates with data hooks. |
|
|
60
103
|
| `JBRulesets` | Stores and manages project rulesets. Handles queuing, cycling, weight decay, approval hook validation, and weight caching for long-running projects. |
|
|
61
|
-
| `JBTokens` | Manages dual-balance token accounting (credits + ERC-20). Credits are minted by default; once an ERC-20 is deployed, credits can be claimed as tokens. |
|
|
62
|
-
| `JBSplits` | Stores split configurations per project, ruleset, and group. Splits route percentages of payouts or reserved tokens to beneficiaries, projects, or hooks. |
|
|
63
|
-
| `JBFundAccessLimits` | Stores payout limits and surplus allowances per project, ruleset, terminal, and token. Limits are denominated in configurable currencies. |
|
|
64
|
-
| `JBPrices` | Price feed registry. Maps currency pairs to `IJBPriceFeed` implementations, with per-project overrides and protocol-wide defaults. |
|
|
104
|
+
| `JBTokens` | Manages dual-balance token accounting (credits + ERC-20). Credits are minted by default; once an ERC-20 is deployed or set, credits can be claimed as tokens. Credits are burned before ERC-20 tokens. |
|
|
105
|
+
| `JBSplits` | Stores split configurations per project, ruleset, and group. Splits route percentages of payouts or reserved tokens to beneficiaries, projects, or hooks. Packed storage for gas efficiency. Falls back to ruleset ID 0 if no splits are set for a specific ruleset. |
|
|
106
|
+
| `JBFundAccessLimits` | Stores payout limits and surplus allowances per project, ruleset, terminal, and token. Limits are denominated in configurable currencies and must be set in strictly increasing currency order to prevent duplicates. |
|
|
107
|
+
| `JBPrices` | Price feed registry. Maps currency pairs to `IJBPriceFeed` implementations, with per-project overrides and protocol-wide defaults. Feeds are immutable once set. Inverse prices are auto-calculated. |
|
|
65
108
|
|
|
66
|
-
### Token
|
|
109
|
+
### Token and Price Feed Contracts
|
|
67
110
|
|
|
68
111
|
| Contract | Description |
|
|
69
112
|
|----------|-------------|
|
|
70
|
-
| `JBERC20` | Cloneable ERC-20 with ERC20Votes and ERC20Permit. Deployed by `JBTokens`
|
|
71
|
-
| `JBChainlinkV3PriceFeed` | `IJBPriceFeed` backed by a Chainlink `AggregatorV3Interface` with staleness
|
|
72
|
-
| `JBChainlinkV3SequencerPriceFeed` | Extends `JBChainlinkV3PriceFeed` with L2 sequencer uptime validation for Optimism/Arbitrum. |
|
|
73
|
-
| `JBMatchingPriceFeed` | Returns 1:1 price (e.g
|
|
113
|
+
| `JBERC20` | Cloneable ERC-20 with ERC20Votes and ERC20Permit. Deployed by `JBTokens` via `Clones.clone()`. Owned by `JBTokens`. |
|
|
114
|
+
| `JBChainlinkV3PriceFeed` | `IJBPriceFeed` backed by a Chainlink `AggregatorV3Interface` with staleness threshold. Rejects negative/zero prices and incomplete rounds. |
|
|
115
|
+
| `JBChainlinkV3SequencerPriceFeed` | Extends `JBChainlinkV3PriceFeed` with L2 sequencer uptime validation and grace period for Optimism/Arbitrum. |
|
|
116
|
+
| `JBMatchingPriceFeed` | Returns 1:1 price (e.g., ETH/NATIVE_TOKEN on applicable chains). Lives in `src/periphery/`. |
|
|
74
117
|
|
|
75
118
|
### Utility Contracts
|
|
76
119
|
|
|
77
120
|
| Contract | Description |
|
|
78
121
|
|----------|-------------|
|
|
79
|
-
| `JBFeelessAddresses` | Owner-managed allowlist of addresses exempt from terminal fees. |
|
|
122
|
+
| `JBFeelessAddresses` | Owner-managed allowlist of addresses exempt from terminal fees. Supports `IERC165`. |
|
|
80
123
|
| `JBDeadline` | Approval hook that rejects rulesets queued too close to the current ruleset's end. Ships as `JBDeadline3Hours`, `JBDeadline1Day`, `JBDeadline3Days`, `JBDeadline7Days`. |
|
|
81
124
|
|
|
125
|
+
### Abstract Contracts
|
|
126
|
+
|
|
127
|
+
| Contract | Description |
|
|
128
|
+
|----------|-------------|
|
|
129
|
+
| `JBControlled` | Provides `onlyControllerOf(projectId)` modifier. Used by `JBRulesets`, `JBTokens`, `JBSplits`, `JBFundAccessLimits`, and `JBPrices`. |
|
|
130
|
+
| `JBPermissioned` | Provides `_requirePermissionFrom` and `_requirePermissionAllowingOverrideFrom` helpers. Used by `JBController`, `JBMultiTerminal`, `JBDirectory`, and `JBPrices`. |
|
|
131
|
+
|
|
82
132
|
### Libraries
|
|
83
133
|
|
|
84
134
|
| Library | Description |
|
|
@@ -86,12 +136,22 @@ Juicebox V6 separates concerns across specialized contracts that coordinate thro
|
|
|
86
136
|
| `JBConstants` | Protocol-wide constants: `NATIVE_TOKEN` address, max percentages, max fee. |
|
|
87
137
|
| `JBCurrencyIds` | Currency identifiers (`ETH = 1`, `USD = 2`). |
|
|
88
138
|
| `JBSplitGroupIds` | Group identifiers (`RESERVED_TOKENS = 1`). |
|
|
89
|
-
| `JBCashOuts` | Bonding curve math for computing cash out reclaim amounts. |
|
|
90
|
-
| `JBSurplus` | Calculates a project's surplus across terminals. |
|
|
91
|
-
| `JBFees` | Fee calculation helpers. |
|
|
92
|
-
| `JBFixedPointNumber` | Decimal adjustment
|
|
93
|
-
| `JBMetadataResolver` | Packs and unpacks metadata
|
|
94
|
-
| `JBRulesetMetadataResolver` | Packs and unpacks the `uint256 metadata` field on `JBRuleset` into `JBRulesetMetadata`. |
|
|
139
|
+
| `JBCashOuts` | Bonding curve math for computing cash out reclaim amounts. Includes `minCashOutCountFor` inverse via binary search. |
|
|
140
|
+
| `JBSurplus` | Calculates a project's surplus across all terminals. |
|
|
141
|
+
| `JBFees` | Fee calculation helpers. `feeAmountFrom` (forward) and `feeAmountResultingIn` (backward). |
|
|
142
|
+
| `JBFixedPointNumber` | Decimal adjustment between fixed-point number precisions. |
|
|
143
|
+
| `JBMetadataResolver` | Packs and unpacks variable-length `{id: data}` metadata entries with a lookup table. Used by pay/cash-out hooks. |
|
|
144
|
+
| `JBRulesetMetadataResolver` | Packs and unpacks the `uint256 metadata` field on `JBRuleset` into `JBRulesetMetadata`. Bit layout: version (4 bits), reservedPercent (16), cashOutTaxRate (16), baseCurrency (32), 14 boolean flags (1 bit each), dataHook address (160), metadata (14). |
|
|
145
|
+
|
|
146
|
+
### Hook Interfaces
|
|
147
|
+
|
|
148
|
+
| Interface | Description |
|
|
149
|
+
|-----------|-------------|
|
|
150
|
+
| `IJBRulesetApprovalHook` | Determines whether the next queued ruleset is approved or rejected. Must implement `approvalStatusOf` and `DURATION`. |
|
|
151
|
+
| `IJBRulesetDataHook` | Overrides payment/cash-out parameters. Implements `beforePayRecordedWith`, `beforeCashOutRecordedWith`, and `hasMintPermissionFor`. |
|
|
152
|
+
| `IJBPayHook` | Called after a payment is recorded. Implements `afterPayRecordedWith`. |
|
|
153
|
+
| `IJBCashOutHook` | Called after a cash out is recorded. Implements `afterCashOutRecordedWith`. |
|
|
154
|
+
| `IJBSplitHook` | Called when processing a split. Implements `processSplitWith`. |
|
|
95
155
|
|
|
96
156
|
## Install
|
|
97
157
|
|
package/SKILLS.md
CHANGED
|
@@ -12,44 +12,137 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
12
12
|
| `JBPermissions` | Packed `uint256` bitmap permissions. Operators get specific permission IDs scoped to projects. |
|
|
13
13
|
| `JBDirectory` | Maps project IDs to their controller (`IERC165`) and terminals (`IJBTerminal[]`). |
|
|
14
14
|
| `JBController` | Orchestrates rulesets, tokens, splits, fund access limits. Entry point for project lifecycle. |
|
|
15
|
-
| `JBMultiTerminal` | Handles ETH/ERC-20 payments, cash outs, payouts, surplus allowance, fees. |
|
|
15
|
+
| `JBMultiTerminal` | Handles ETH/ERC-20 payments, cash outs, payouts, surplus allowance, fees. Permit2 integration. |
|
|
16
16
|
| `JBTerminalStore` | Bookkeeping: balances, payout limit tracking, surplus calculation, bonding curve reclaim math. |
|
|
17
17
|
| `JBRulesets` | Stores/cycles rulesets with weight decay, approval hooks, and weight cache for gas-efficient long-running cycles. |
|
|
18
18
|
| `JBTokens` | Dual-balance system: credits (internal) + ERC-20. Credits burned first on burn. |
|
|
19
19
|
| `JBSplits` | Split configurations per project/ruleset/group. Packed storage for gas efficiency. |
|
|
20
20
|
| `JBFundAccessLimits` | Payout limits and surplus allowances per project/ruleset/terminal/token. |
|
|
21
|
-
| `JBPrices` | Price feed registry with project-specific and protocol-wide default feeds. |
|
|
22
|
-
| `JBERC20` | Cloneable ERC-20 with Votes + Permit. Owned by `JBTokens`. |
|
|
21
|
+
| `JBPrices` | Price feed registry with project-specific and protocol-wide default feeds. Immutable once set. |
|
|
22
|
+
| `JBERC20` | Cloneable ERC-20 with Votes + Permit. Owned by `JBTokens`. Deployed via `Clones.clone()`. |
|
|
23
23
|
| `JBFeelessAddresses` | Allowlist for fee-exempt addresses. |
|
|
24
|
-
| `JBChainlinkV3PriceFeed` | Chainlink AggregatorV3 price feed with staleness threshold. |
|
|
25
|
-
| `JBChainlinkV3SequencerPriceFeed` | L2 sequencer-aware Chainlink feed (Optimism/Arbitrum). |
|
|
26
|
-
| `JBDeadline` | Approval hook: rejects rulesets queued within `DURATION` seconds of start. |
|
|
27
|
-
| `JBMatchingPriceFeed` | Always returns 1:1. For equivalent currencies. |
|
|
24
|
+
| `JBChainlinkV3PriceFeed` | Chainlink AggregatorV3 price feed with staleness threshold. Rejects negative/zero prices. |
|
|
25
|
+
| `JBChainlinkV3SequencerPriceFeed` | L2 sequencer-aware Chainlink feed (Optimism/Arbitrum) with grace period after restart. |
|
|
26
|
+
| `JBDeadline` | Approval hook: rejects rulesets queued within `DURATION` seconds of start. Ships as `JBDeadline3Hours`, `JBDeadline1Day`, `JBDeadline3Days`, `JBDeadline7Days`. |
|
|
27
|
+
| `JBMatchingPriceFeed` | Always returns 1:1. For equivalent currencies (e.g. ETH/NATIVE_TOKEN). |
|
|
28
28
|
|
|
29
29
|
## Key Functions
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
31
|
+
### JBController
|
|
32
|
+
|
|
33
|
+
| Function | What it does |
|
|
34
|
+
|----------|--------------|
|
|
35
|
+
| `launchProjectFor(address owner, string uri, JBRulesetConfig[] rulesetConfigs, JBTerminalConfig[] terminalConfigs, string memo)` | Creates a project, queues its first rulesets, and configures terminals. Returns `projectId`. |
|
|
36
|
+
| `launchRulesetsFor(uint256 projectId, JBRulesetConfig[] rulesetConfigs, JBTerminalConfig[] terminalConfigs, string memo)` | Launches the first rulesets for an existing project that has none. |
|
|
37
|
+
| `queueRulesetsOf(uint256 projectId, JBRulesetConfig[] rulesetConfigs, string memo)` | Queues new rulesets for a project. Takes effect after the current ruleset ends (or immediately if duration is 0). |
|
|
38
|
+
| `mintTokensOf(uint256 projectId, uint256 tokenCount, address beneficiary, string memo, bool useReservedPercent)` | Mints project tokens. Requires `allowOwnerMinting` in the current ruleset or caller must be a terminal/hook with mint permission. |
|
|
39
|
+
| `burnTokensOf(address holder, uint256 projectId, uint256 tokenCount, string memo)` | Burns tokens from a holder. Requires holder's permission (`BURN_TOKENS`). |
|
|
40
|
+
| `sendReservedTokensToSplitsOf(uint256 projectId)` | Distributes accumulated reserved tokens to the reserved token split group. Returns token count sent. |
|
|
41
|
+
| `deployERC20For(uint256 projectId, string name, string symbol, bytes32 salt)` | Deploys a cloneable `JBERC20` for the project. Credits become claimable. |
|
|
42
|
+
| `claimTokensFor(address holder, uint256 projectId, uint256 count, address beneficiary)` | Redeems credits for ERC-20 tokens into beneficiary's wallet. |
|
|
43
|
+
| `setSplitGroupsOf(uint256 projectId, uint256 rulesetId, JBSplitGroup[] splitGroups)` | Sets the split groups for a project's ruleset. |
|
|
44
|
+
| `setTokenFor(uint256 projectId, IJBToken token)` | Sets an existing ERC-20 token for the project (requires `allowSetCustomToken` in ruleset). |
|
|
45
|
+
| `setUriOf(uint256 projectId, string uri)` | Sets the project's metadata URI. |
|
|
46
|
+
| `transferCreditsFrom(address holder, uint256 projectId, address recipient, uint256 creditCount)` | Transfers credits between addresses (reverts if `pauseCreditTransfers` is set in ruleset). |
|
|
47
|
+
| `addPriceFeedFor(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, IJBPriceFeed feed)` | Registers a price feed (requires `allowAddPriceFeed` in ruleset). |
|
|
48
|
+
| `migrateController(uint256 projectId, IJBMigratable to)` | Migrates the project to a new controller. Calls `beforeReceiveMigrationFrom`, `migrate`, updates directory, then `afterReceiveMigrationFrom`. |
|
|
49
|
+
| `currentRulesetOf(uint256 projectId)` | Returns the current ruleset and unpacked metadata. |
|
|
50
|
+
| `upcomingRulesetOf(uint256 projectId)` | Returns the upcoming ruleset and unpacked metadata. |
|
|
51
|
+
| `allRulesetsOf(uint256 projectId, uint256 startingId, uint256 size)` | Returns an array of rulesets with metadata, paginated. |
|
|
52
|
+
| `pendingReservedTokenBalanceOf(uint256 projectId)` | Returns accumulated reserved tokens not yet distributed. |
|
|
53
|
+
| `totalTokenSupplyWithReservedTokensOf(uint256 projectId)` | Returns total supply including pending reserved tokens. |
|
|
54
|
+
|
|
55
|
+
### JBMultiTerminal
|
|
56
|
+
|
|
57
|
+
| Function | What it does |
|
|
58
|
+
|----------|--------------|
|
|
59
|
+
| `pay(uint256 projectId, address token, uint256 amount, address beneficiary, uint256 minReturnedTokens, string memo, bytes metadata)` | Pays a project. Mints project tokens to beneficiary based on ruleset weight. Returns token count. |
|
|
60
|
+
| `cashOutTokensOf(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, uint256 minTokensReclaimed, address beneficiary, bytes metadata)` | Burns project tokens and reclaims surplus terminal tokens via bonding curve. |
|
|
61
|
+
| `sendPayoutsOf(uint256 projectId, address token, uint256 amount, uint256 currency, uint256 minTokensPaidOut)` | Distributes payouts from the project's balance to its payout split group, up to the payout limit. |
|
|
62
|
+
| `useAllowanceOf(uint256 projectId, address token, uint256 amount, uint256 currency, uint256 minTokensPaidOut, address payable beneficiary, address payable feeBeneficiary, string memo)` | Withdraws from the project's surplus allowance to a beneficiary. The `feeBeneficiary` receives tokens minted by the fee payment. |
|
|
63
|
+
| `addToBalanceOf(uint256 projectId, address token, uint256 amount, bool shouldReturnHeldFees, string memo, bytes metadata)` | Adds funds to a project's balance without minting tokens. Can unlock held fees. |
|
|
64
|
+
| `migrateBalanceOf(uint256 projectId, address token, IJBTerminal to)` | Migrates a project's token balance to another terminal. Requires `allowTerminalMigration`. |
|
|
65
|
+
| `processHeldFeesOf(uint256 projectId, address token, uint256 count)` | Processes up to `count` held fees for a project, sending them to the fee beneficiary project. |
|
|
66
|
+
| `addAccountingContextsFor(uint256 projectId, JBAccountingContext[] accountingContexts)` | Adds new accounting contexts (token types) to a terminal for a project. |
|
|
67
|
+
| `currentSurplusOf(uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the project's current surplus in the specified currency. |
|
|
68
|
+
| `accountingContextForTokenOf(uint256 projectId, address token)` | Returns the accounting context for a specific token. |
|
|
69
|
+
| `accountingContextsOf(uint256 projectId)` | Returns all accounting contexts for a project. |
|
|
70
|
+
| `heldFeesOf(uint256 projectId, address token, uint256 count)` | Returns up to `count` held fees for a project/token. |
|
|
71
|
+
|
|
72
|
+
### JBTerminalStore
|
|
73
|
+
|
|
74
|
+
| Function | What it does |
|
|
75
|
+
|----------|--------------|
|
|
76
|
+
| `recordPaymentFrom(address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` | Records a payment. Applies data hook if enabled. Returns ruleset, token count, hook specifications. |
|
|
77
|
+
| `recordPayoutFor(uint256 projectId, JBAccountingContext accountingContext, uint256 amount, uint256 currency)` | Records a payout. Enforces payout limits. Returns ruleset and amount paid out. |
|
|
78
|
+
| `recordCashOutFor(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bytes metadata)` | Records a cash out. Computes reclaim via bonding curve. Returns ruleset, reclaim amount, tax rate, and hook specifications. |
|
|
79
|
+
| `recordUsedAllowanceOf(uint256 projectId, JBAccountingContext accountingContext, uint256 amount, uint256 currency)` | Records surplus allowance usage. Enforces allowance limits. Returns ruleset and used amount. |
|
|
80
|
+
| `recordAddedBalanceFor(uint256 projectId, address token, uint256 amount)` | Records funds added to a project's balance. |
|
|
81
|
+
| `recordTerminalMigration(uint256 projectId, address token)` | Records a terminal migration, returning the full balance. |
|
|
82
|
+
| `balanceOf(address terminal, uint256 projectId, address token)` | Returns the balance of a project at a terminal for a given token. |
|
|
83
|
+
| `usedPayoutLimitOf(address terminal, uint256 projectId, address token, uint256 rulesetCycleNumber, uint256 currency)` | Returns the used payout limit for a project in a given cycle. |
|
|
84
|
+
| `usedSurplusAllowanceOf(address terminal, uint256 projectId, address token, uint256 rulesetId, uint256 currency)` | Returns the used surplus allowance for a project in a given ruleset. |
|
|
85
|
+
| `currentSurplusOf(address terminal, uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the current surplus for a project at a terminal. |
|
|
86
|
+
|
|
87
|
+
### JBRulesets
|
|
88
|
+
|
|
89
|
+
| Function | What it does |
|
|
90
|
+
|----------|--------------|
|
|
91
|
+
| `currentOf(uint256 projectId)` | Returns the currently active ruleset with decayed weight and correct cycle number. |
|
|
92
|
+
| `latestQueuedOf(uint256 projectId)` | Returns the latest queued ruleset and its approval status. |
|
|
93
|
+
| `queueFor(uint256 projectId, uint256 duration, uint256 weight, uint256 weightCutPercent, IJBRulesetApprovalHook approvalHook, uint256 metadata, uint256 mustStartAtOrAfter)` | Queues a new ruleset. Only callable by the project's controller. |
|
|
94
|
+
| `updateRulesetWeightCache(uint256 projectId)` | Updates the weight cache for long-running rulesets. Required when `weightCutMultiple > 20,000` to avoid gas limits. |
|
|
95
|
+
|
|
96
|
+
### JBPermissions
|
|
97
|
+
|
|
98
|
+
| Function | What it does |
|
|
99
|
+
|----------|--------------|
|
|
100
|
+
| `setPermissionsFor(address account, JBPermissionsData permissionsData)` | Grants or revokes operator permissions. ROOT operators can set non-ROOT permissions for others. |
|
|
101
|
+
| `hasPermission(address operator, address account, uint256 projectId, uint256 permissionId)` | Checks if an operator has a specific permission. |
|
|
102
|
+
| `hasPermissions(address operator, address account, uint256 projectId, uint256[] permissionIds)` | Checks if an operator has all specified permissions. |
|
|
103
|
+
|
|
104
|
+
### JBDirectory
|
|
105
|
+
|
|
106
|
+
| Function | What it does |
|
|
107
|
+
|----------|--------------|
|
|
108
|
+
| `controllerOf(uint256 projectId)` | Returns the project's controller as `IERC165`. |
|
|
109
|
+
| `terminalsOf(uint256 projectId)` | Returns the project's terminals as `IJBTerminal[]`. |
|
|
110
|
+
| `primaryTerminalOf(uint256 projectId, address token)` | Returns the project's primary terminal for a given token. |
|
|
111
|
+
| `isTerminalOf(uint256 projectId, IJBTerminal terminal)` | Checks if a terminal belongs to a project. |
|
|
112
|
+
| `setControllerOf(uint256 projectId, IERC165 controller)` | Sets the project's controller. |
|
|
113
|
+
| `setTerminalsOf(uint256 projectId, IJBTerminal[] terminals)` | Sets the project's terminals. |
|
|
114
|
+
| `setPrimaryTerminalOf(uint256 projectId, address token, IJBTerminal terminal)` | Sets the primary terminal for a token. |
|
|
115
|
+
| `setIsAllowedToSetFirstController(address addr, bool flag)` | Allows/disallows an address to set a project's first controller. Owner-only. |
|
|
116
|
+
|
|
117
|
+
### JBPrices
|
|
118
|
+
|
|
119
|
+
| Function | What it does |
|
|
120
|
+
|----------|--------------|
|
|
121
|
+
| `pricePerUnitOf(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, uint256 decimals)` | Returns the price of 1 `unitCurrency` in `pricingCurrency`. Checks project-specific, inverse, then default feeds. |
|
|
122
|
+
| `addPriceFeedFor(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, IJBPriceFeed feed)` | Registers a price feed. Project ID 0 sets protocol-wide defaults (owner-only). Immutable once set. |
|
|
123
|
+
|
|
124
|
+
### JBTokens
|
|
125
|
+
|
|
126
|
+
| Function | What it does |
|
|
127
|
+
|----------|--------------|
|
|
128
|
+
| `totalSupplyOf(uint256 projectId)` | Returns total supply: credits + ERC-20 tokens. |
|
|
129
|
+
| `totalBalanceOf(address holder, uint256 projectId)` | Returns combined credit + ERC-20 balance. |
|
|
130
|
+
| `creditBalanceOf(address holder, uint256 projectId)` | Returns the holder's credit balance. |
|
|
131
|
+
| `tokenOf(uint256 projectId)` | Returns the ERC-20 token for a project (`IJBToken`). |
|
|
132
|
+
|
|
133
|
+
### JBSplits
|
|
134
|
+
|
|
135
|
+
| Function | What it does |
|
|
136
|
+
|----------|--------------|
|
|
137
|
+
| `splitsOf(uint256 projectId, uint256 rulesetId, uint256 groupId)` | Returns splits for a project/ruleset/group. Falls back to ruleset ID 0 if none set. |
|
|
138
|
+
|
|
139
|
+
### Other
|
|
140
|
+
|
|
141
|
+
| Function | What it does |
|
|
142
|
+
|----------|--------------|
|
|
143
|
+
| `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the fee exemption list. Owner-only. (`JBFeelessAddresses`) |
|
|
144
|
+
| `setControllerAllowed(uint256 projectId)` | Returns whether a project's controller can currently be set. (`IJBDirectoryAccessControl`) |
|
|
145
|
+
| `setTerminalsAllowed(uint256 projectId)` | Returns whether a project's terminals can currently be set. (`IJBDirectoryAccessControl`) |
|
|
53
146
|
|
|
54
147
|
## Key Types
|
|
55
148
|
|
|
@@ -69,8 +162,21 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
69
162
|
| `JBFee` | `amount (uint256)`, `beneficiary (address)`, `unlockTimestamp (uint48)` | Held fees in `JBMultiTerminal` |
|
|
70
163
|
| `JBSingleAllowance` | `sigDeadline (uint256)`, `amount (uint160)`, `expiration (uint48)`, `nonce (uint48)`, `signature (bytes)` | Permit2 allowance in terminal payments |
|
|
71
164
|
| `JBRulesetWithMetadata` | `ruleset (JBRuleset)`, `metadata (JBRulesetMetadata)` | `allRulesetsOf()`, `currentRulesetOf()` return values |
|
|
165
|
+
| `JBRulesetWeightCache` | `weight (uint112)`, `weightCutMultiple (uint168)` | Weight caching for long-running rulesets in `JBRulesets` |
|
|
72
166
|
| `JBApprovalStatus` (enum) | `Empty`, `Upcoming`, `Active`, `ApprovalExpected`, `Approved`, `Failed` | Approval hook status for queued rulesets |
|
|
73
167
|
|
|
168
|
+
### Hook Structs
|
|
169
|
+
|
|
170
|
+
| Struct | Key Fields | Used In |
|
|
171
|
+
|--------|------------|---------|
|
|
172
|
+
| `JBBeforePayRecordedContext` | `terminal`, `payer`, `amount (JBTokenAmount)`, `projectId`, `rulesetId`, `beneficiary`, `weight`, `reservedPercent`, `metadata` | `IJBRulesetDataHook.beforePayRecordedWith()` input |
|
|
173
|
+
| `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `useTotalSurplus`, `cashOutTaxRate`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
|
|
174
|
+
| `JBAfterPayRecordedContext` | `payer`, `projectId`, `rulesetId`, `amount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `weight`, `newlyIssuedTokenCount`, `beneficiary`, `hookMetadata`, `payerMetadata` | `IJBPayHook.afterPayRecordedWith()` input |
|
|
175
|
+
| `JBAfterCashOutRecordedContext` | `holder`, `projectId`, `rulesetId`, `cashOutCount`, `reclaimedAmount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `cashOutTaxRate`, `beneficiary`, `hookMetadata`, `cashOutMetadata` | `IJBCashOutHook.afterCashOutRecordedWith()` input |
|
|
176
|
+
| `JBPayHookSpecification` | `hook (IJBPayHook)`, `amount`, `metadata` | Returned by data hook; specifies which pay hooks to call and how much to forward |
|
|
177
|
+
| `JBCashOutHookSpecification` | `hook (IJBCashOutHook)`, `amount`, `metadata` | Returned by data hook; specifies which cash out hooks to call and how much to forward |
|
|
178
|
+
| `JBSplitHookContext` | `token`, `amount`, `decimals`, `projectId`, `groupId`, `split (JBSplit)` | `IJBSplitHook.processSplitWith()` input |
|
|
179
|
+
|
|
74
180
|
### Constants (`JBConstants`)
|
|
75
181
|
|
|
76
182
|
| Constant | Value | Meaning |
|
|
@@ -89,6 +195,24 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
89
195
|
| `1` | ETH |
|
|
90
196
|
| `2` | USD |
|
|
91
197
|
|
|
198
|
+
### Split Group IDs (`JBSplitGroupIds`)
|
|
199
|
+
|
|
200
|
+
| ID | Group |
|
|
201
|
+
|----|-------|
|
|
202
|
+
| `1` | `RESERVED_TOKENS` -- reserved token distribution |
|
|
203
|
+
|
|
204
|
+
### Special Values
|
|
205
|
+
|
|
206
|
+
| Value | Context | Meaning |
|
|
207
|
+
|-------|---------|---------|
|
|
208
|
+
| `weight = 0` | `JBRuleset` / `JBRulesetConfig` | No token issuance for payments. |
|
|
209
|
+
| `weight = 1` | `JBRuleset` / `JBRulesetConfig` | Inherit decayed weight from previous ruleset (sentinel). |
|
|
210
|
+
| `duration = 0` | `JBRuleset` / `JBRulesetConfig` | Ruleset never expires; must be explicitly replaced by a new queued ruleset (takes effect immediately). |
|
|
211
|
+
| `projectId = 0` | `JBPermissionsData` | Wildcard: permission applies to ALL projects. Cannot be combined with ROOT (255). |
|
|
212
|
+
| `permissionId = 255` | `JBPermissions` | ROOT: grants all permissions for the scoped project. |
|
|
213
|
+
| `rulesetId = 0` | `JBSplits.splitsOf()` | Fallback split group used when no splits are set for a specific ruleset. |
|
|
214
|
+
| `projectId = 0` | `JBPrices.addPriceFeedFor()` | Sets a protocol-wide default price feed (owner-only). |
|
|
215
|
+
|
|
92
216
|
## Gotchas
|
|
93
217
|
|
|
94
218
|
- `IJBDirectory.controllerOf()` returns `IERC165`, NOT `address` -- must wrap: `address(directory.controllerOf(projectId))`
|
|
@@ -110,12 +234,24 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
110
234
|
- `JBProjects` constructor optionally mints project #1 to `feeProjectOwner` -- if `address(0)`, no fee project is created
|
|
111
235
|
- `JBMultiTerminal` derives `DIRECTORY` and `RULESETS` from the provided `store` in its constructor -- not passed directly
|
|
112
236
|
- `JBPrices.pricePerUnitOf()` checks project-specific feed, then inverse, then falls back to `DEFAULT_PROJECT_ID = 0`
|
|
237
|
+
- `useAllowanceOf()` takes 8 args including `address payable feeBeneficiary` -- do NOT omit it
|
|
238
|
+
- Cash out tax rate of 0% = proportional (1:1) redemption; 100% = nothing reclaimable (all surplus locked). Do NOT confuse with a "cash out rate" where 100% means full redemption.
|
|
239
|
+
- `cashOutTaxRate` in `JBRulesetMetadata` is `uint16` (max 10,000 basis points), NOT 9-decimal precision
|
|
240
|
+
- `reservedPercent` in `JBRulesetMetadata` is `uint16` (max 10,000 basis points), NOT 9-decimal precision
|
|
241
|
+
- `weight` in `JBRuleset` is `uint112`, but `weight` in `JBRulesetConfig` is also `uint112` -- both use 18 decimals
|
|
242
|
+
- `JBSplits.splitsOf()` falls back to ruleset ID 0 if no splits are set for the given rulesetId
|
|
243
|
+
- Held fees are held for 28 days (`_FEE_HOLDING_SECONDS = 2,419,200`) before they can be processed
|
|
244
|
+
- `JBController`, `JBMultiTerminal`, `JBProjects`, `JBPrices`, `JBPermissions` all support ERC-2771 meta-transactions
|
|
245
|
+
- `JBRulesetMetadataResolver` bit layout: version (4 bits), reservedPercent (16), cashOutTaxRate (16), baseCurrency (32), 14 boolean flags (1 bit each), dataHook address (160), metadata (14)
|
|
246
|
+
- `IJBDirectoryAccessControl` has `setControllerAllowed()` and `setTerminalsAllowed()` -- NOT `setControllerAllowedFor()`
|
|
247
|
+
- Price feeds are immutable once set in `JBPrices` -- they cannot be replaced or removed
|
|
248
|
+
- `JBFundAccessLimits` requires payout limits and surplus allowances to be in strictly increasing currency order to prevent duplicates
|
|
113
249
|
|
|
114
250
|
## Example Integration
|
|
115
251
|
|
|
116
252
|
```solidity
|
|
117
253
|
// SPDX-License-Identifier: MIT
|
|
118
|
-
pragma solidity 0.8.
|
|
254
|
+
pragma solidity 0.8.26;
|
|
119
255
|
|
|
120
256
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
121
257
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
package/package.json
CHANGED
|
@@ -123,7 +123,7 @@ library JBMetadataResolver {
|
|
|
123
123
|
// Pad as needed - inlined for gas saving
|
|
124
124
|
uint256 paddedLength =
|
|
125
125
|
newMetadata.length % WORD_SIZE == 0 ? newMetadata.length : (newMetadata.length / WORD_SIZE + 1) * WORD_SIZE;
|
|
126
|
-
assembly {
|
|
126
|
+
assembly ("memory-safe") {
|
|
127
127
|
mstore(newMetadata, paddedLength)
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -135,7 +135,7 @@ library JBMetadataResolver {
|
|
|
135
135
|
// Pad as needed
|
|
136
136
|
paddedLength =
|
|
137
137
|
newMetadata.length % WORD_SIZE == 0 ? newMetadata.length : (newMetadata.length / WORD_SIZE + 1) * WORD_SIZE;
|
|
138
|
-
assembly {
|
|
138
|
+
assembly ("memory-safe") {
|
|
139
139
|
mstore(newMetadata, paddedLength)
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -146,7 +146,7 @@ library JBMetadataResolver {
|
|
|
146
146
|
paddedLength =
|
|
147
147
|
newMetadata.length % WORD_SIZE == 0 ? newMetadata.length : (newMetadata.length / WORD_SIZE + 1) * WORD_SIZE;
|
|
148
148
|
|
|
149
|
-
assembly {
|
|
149
|
+
assembly ("memory-safe") {
|
|
150
150
|
mstore(newMetadata, paddedLength)
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -192,7 +192,7 @@ library JBMetadataResolver {
|
|
|
192
192
|
uint256 paddedLength = metadata.length % JBMetadataResolver.WORD_SIZE == 0
|
|
193
193
|
? metadata.length
|
|
194
194
|
: (metadata.length / JBMetadataResolver.WORD_SIZE + 1) * JBMetadataResolver.WORD_SIZE;
|
|
195
|
-
assembly {
|
|
195
|
+
assembly ("memory-safe") {
|
|
196
196
|
mstore(metadata, paddedLength)
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -206,7 +206,7 @@ library JBMetadataResolver {
|
|
|
206
206
|
? metadata.length
|
|
207
207
|
: (metadata.length / JBMetadataResolver.WORD_SIZE + 1) * JBMetadataResolver.WORD_SIZE;
|
|
208
208
|
|
|
209
|
-
assembly {
|
|
209
|
+
assembly ("memory-safe") {
|
|
210
210
|
mstore(metadata, paddedLength)
|
|
211
211
|
}
|
|
212
212
|
}
|
|
@@ -231,7 +231,7 @@ library JBMetadataResolver {
|
|
|
231
231
|
uint256 currentOffset = uint256(uint8(metadata[i + ID_SIZE]));
|
|
232
232
|
|
|
233
233
|
bytes4 parsedId;
|
|
234
|
-
assembly {
|
|
234
|
+
assembly ("memory-safe") {
|
|
235
235
|
parsedId := mload(add(add(metadata, 0x20), i))
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -279,7 +279,7 @@ library JBMetadataResolver {
|
|
|
279
279
|
pure
|
|
280
280
|
returns (bytes memory slicedBytes)
|
|
281
281
|
{
|
|
282
|
-
assembly {
|
|
282
|
+
assembly ("memory-safe") {
|
|
283
283
|
let length := sub(end, start)
|
|
284
284
|
|
|
285
285
|
// M-20: Allocate memory at freemem — round up to 32-byte boundary so subsequent
|
|
@@ -262,13 +262,14 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
// ═══════════════════════════════════════════════════════════════════
|
|
265
|
-
// Test 5: C-5
|
|
265
|
+
// Test 5: C-5 regression — cashOut(0) with totalSupply==0 must return 0
|
|
266
266
|
// ═══════════════════════════════════════════════════════════════════
|
|
267
267
|
|
|
268
|
-
/// @notice C-5
|
|
269
|
-
/// @dev
|
|
270
|
-
///
|
|
271
|
-
///
|
|
268
|
+
/// @notice C-5 was a V5 audit finding where cashOut(0) with totalSupply==0 returned the entire surplus.
|
|
269
|
+
/// @dev In V5, `cashOutCount >= totalSupply` (0 >= 0) was true and returned the full surplus before
|
|
270
|
+
/// checking for zero cashOutCount. Fixed since V5.1: `JBCashOuts.cashOutFrom` returns 0 when
|
|
271
|
+
/// cashOutCount==0 (line 31) before reaching the `cashOutCount >= totalSupply` check (line 37).
|
|
272
|
+
/// This test verifies the fix holds.
|
|
272
273
|
function test_C5_variant_addToBalance_zeroCashOut() public {
|
|
273
274
|
// Add to balance when no tokens exist
|
|
274
275
|
vm.deal(address(0xD000), 5 ether);
|
|
@@ -282,7 +283,7 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
|
|
|
282
283
|
metadata: new bytes(0)
|
|
283
284
|
});
|
|
284
285
|
|
|
285
|
-
//
|
|
286
|
+
// cashOut(0) with totalSupply==0 must reclaim nothing.
|
|
286
287
|
address attacker = address(0xA77AC0);
|
|
287
288
|
vm.prank(attacker);
|
|
288
289
|
uint256 reclaimAmount = jbMultiTerminal()
|
|
@@ -296,13 +297,7 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
|
|
|
296
297
|
metadata: new bytes(0)
|
|
297
298
|
});
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
// Documenting the known critical finding.
|
|
301
|
-
if (reclaimAmount > 0) {
|
|
302
|
-
emit log_named_uint("C-5 CONFIRMED: cashOut(0) extracted surplus", reclaimAmount);
|
|
303
|
-
}
|
|
304
|
-
// Test passes either way to document behavior
|
|
305
|
-
assertTrue(true, "C-5 documented");
|
|
300
|
+
assertEq(reclaimAmount, 0, "C-5 regression: cashOut(0) must return 0");
|
|
306
301
|
}
|
|
307
302
|
|
|
308
303
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.6;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {CommonBase} from "forge-std/Base.sol";
|
|
6
|
+
import {StdCheats} from "forge-std/StdCheats.sol";
|
|
7
|
+
import {StdUtils} from "forge-std/StdUtils.sol";
|
|
8
|
+
|
|
9
|
+
import {JBPermissions} from "../src/JBPermissions.sol";
|
|
10
|
+
import {IJBPermissions} from "../src/interfaces/IJBPermissions.sol";
|
|
11
|
+
import {JBPermissionsData} from "../src/structs/JBPermissionsData.sol";
|
|
12
|
+
|
|
13
|
+
/// @title PermissionsHandler
|
|
14
|
+
/// @notice Stateful handler for JBPermissions invariant testing.
|
|
15
|
+
/// Randomly sets, revokes, and checks permissions while tracking expected state.
|
|
16
|
+
contract PermissionsHandler is CommonBase, StdCheats, StdUtils {
|
|
17
|
+
JBPermissions public immutable PERMISSIONS;
|
|
18
|
+
|
|
19
|
+
address[] public accounts;
|
|
20
|
+
address[] public operators;
|
|
21
|
+
uint56[] public projectIds;
|
|
22
|
+
|
|
23
|
+
// Ghost state: track what we've set.
|
|
24
|
+
// Keyed by keccak256(operator, account, projectId).
|
|
25
|
+
mapping(bytes32 => uint256) public expectedPacked;
|
|
26
|
+
|
|
27
|
+
// Counters.
|
|
28
|
+
uint256 public setCount;
|
|
29
|
+
uint256 public revokeCount;
|
|
30
|
+
uint256 public rootSetCount;
|
|
31
|
+
uint256 public rootForwardAttempts;
|
|
32
|
+
uint256 public rootForwardBlocked;
|
|
33
|
+
uint256 public wildcardSetAttempts;
|
|
34
|
+
uint256 public wildcardSetBlocked;
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
PERMISSIONS = new JBPermissions(address(0));
|
|
38
|
+
|
|
39
|
+
accounts.push(makeAddr("accountA"));
|
|
40
|
+
accounts.push(makeAddr("accountB"));
|
|
41
|
+
accounts.push(makeAddr("accountC"));
|
|
42
|
+
|
|
43
|
+
operators.push(makeAddr("operatorX"));
|
|
44
|
+
operators.push(makeAddr("operatorY"));
|
|
45
|
+
operators.push(makeAddr("operatorZ"));
|
|
46
|
+
|
|
47
|
+
projectIds.push(1);
|
|
48
|
+
projectIds.push(2);
|
|
49
|
+
projectIds.push(3);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// @notice Set random permissions for a random (operator, account, projectId) triple.
|
|
53
|
+
function setPermissions(
|
|
54
|
+
uint256 accountSeed,
|
|
55
|
+
uint256 operatorSeed,
|
|
56
|
+
uint256 projectSeed,
|
|
57
|
+
uint8[] memory permissionIds
|
|
58
|
+
)
|
|
59
|
+
public
|
|
60
|
+
{
|
|
61
|
+
address account = accounts[bound(accountSeed, 0, accounts.length - 1)];
|
|
62
|
+
address operator = operators[bound(operatorSeed, 0, operators.length - 1)];
|
|
63
|
+
uint56 projectId = projectIds[bound(projectSeed, 0, projectIds.length - 1)];
|
|
64
|
+
|
|
65
|
+
// Filter out permission ID 0 (invalid) and truncate long arrays.
|
|
66
|
+
if (permissionIds.length > 10) {
|
|
67
|
+
assembly {
|
|
68
|
+
mstore(permissionIds, 10)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
uint8[] memory validIds = new uint8[](permissionIds.length);
|
|
73
|
+
uint256 validCount;
|
|
74
|
+
for (uint256 i; i < permissionIds.length; i++) {
|
|
75
|
+
if (permissionIds[i] > 0) {
|
|
76
|
+
validIds[validCount] = permissionIds[i];
|
|
77
|
+
validCount++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resize to valid count.
|
|
82
|
+
uint8[] memory finalIds = new uint8[](validCount);
|
|
83
|
+
for (uint256 i; i < validCount; i++) {
|
|
84
|
+
finalIds[i] = validIds[i];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Track expected state.
|
|
88
|
+
bytes32 key = keccak256(abi.encodePacked(operator, account, projectId));
|
|
89
|
+
uint256 packed;
|
|
90
|
+
for (uint256 i; i < validCount; i++) {
|
|
91
|
+
packed |= uint256(1) << finalIds[i];
|
|
92
|
+
}
|
|
93
|
+
expectedPacked[key] = packed;
|
|
94
|
+
|
|
95
|
+
// Account sets permissions for itself.
|
|
96
|
+
vm.prank(account);
|
|
97
|
+
PERMISSIONS.setPermissionsFor(
|
|
98
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: finalIds})
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
setCount++;
|
|
102
|
+
|
|
103
|
+
// Track ROOT sets.
|
|
104
|
+
for (uint256 i; i < validCount; i++) {
|
|
105
|
+
if (finalIds[i] == 1) {
|
|
106
|
+
rootSetCount++;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// @notice Revoke permissions by setting empty array.
|
|
113
|
+
function revokePermissions(uint256 accountSeed, uint256 operatorSeed, uint256 projectSeed) public {
|
|
114
|
+
address account = accounts[bound(accountSeed, 0, accounts.length - 1)];
|
|
115
|
+
address operator = operators[bound(operatorSeed, 0, operators.length - 1)];
|
|
116
|
+
uint56 projectId = projectIds[bound(projectSeed, 0, projectIds.length - 1)];
|
|
117
|
+
|
|
118
|
+
bytes32 key = keccak256(abi.encodePacked(operator, account, projectId));
|
|
119
|
+
expectedPacked[key] = 0;
|
|
120
|
+
|
|
121
|
+
uint8[] memory emptyIds = new uint8[](0);
|
|
122
|
+
|
|
123
|
+
vm.prank(account);
|
|
124
|
+
PERMISSIONS.setPermissionsFor(
|
|
125
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: emptyIds})
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
revokeCount++;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// @notice Attempt ROOT forwarding (should always be blocked).
|
|
132
|
+
function attemptRootForwarding(uint256 accountSeed, uint256 projectSeed) public {
|
|
133
|
+
address account = accounts[bound(accountSeed, 0, accounts.length - 1)];
|
|
134
|
+
address operator = operators[0]; // operatorX
|
|
135
|
+
address thirdParty = operators[1]; // operatorY
|
|
136
|
+
uint56 projectId = projectIds[bound(projectSeed, 0, projectIds.length - 1)];
|
|
137
|
+
|
|
138
|
+
rootForwardAttempts++;
|
|
139
|
+
|
|
140
|
+
// First give operator ROOT.
|
|
141
|
+
uint8[] memory rootPerms = new uint8[](1);
|
|
142
|
+
rootPerms[0] = 1; // ROOT
|
|
143
|
+
|
|
144
|
+
vm.prank(account);
|
|
145
|
+
PERMISSIONS.setPermissionsFor(
|
|
146
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: rootPerms})
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Update ghost state.
|
|
150
|
+
bytes32 rootKey = keccak256(abi.encodePacked(operator, account, projectId));
|
|
151
|
+
expectedPacked[rootKey] = uint256(1) << 1;
|
|
152
|
+
|
|
153
|
+
// Now operator tries to forward ROOT to thirdParty.
|
|
154
|
+
vm.prank(operator);
|
|
155
|
+
try PERMISSIONS.setPermissionsFor(
|
|
156
|
+
account, JBPermissionsData({operator: thirdParty, projectId: projectId, permissionIds: rootPerms})
|
|
157
|
+
) {
|
|
158
|
+
// Should not reach here.
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
rootForwardBlocked++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// @notice Attempt wildcard permission setting by operator (should be blocked).
|
|
166
|
+
function attemptWildcardByOperator(uint256 accountSeed, uint256 projectSeed) public {
|
|
167
|
+
address account = accounts[bound(accountSeed, 0, accounts.length - 1)];
|
|
168
|
+
address operator = operators[0];
|
|
169
|
+
address thirdParty = operators[1];
|
|
170
|
+
uint56 projectId = projectIds[bound(projectSeed, 0, projectIds.length - 1)];
|
|
171
|
+
|
|
172
|
+
wildcardSetAttempts++;
|
|
173
|
+
|
|
174
|
+
// Give operator ROOT on a specific project.
|
|
175
|
+
uint8[] memory rootPerms = new uint8[](1);
|
|
176
|
+
rootPerms[0] = 1;
|
|
177
|
+
|
|
178
|
+
vm.prank(account);
|
|
179
|
+
PERMISSIONS.setPermissionsFor(
|
|
180
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: rootPerms})
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
bytes32 rootKey = keccak256(abi.encodePacked(operator, account, projectId));
|
|
184
|
+
expectedPacked[rootKey] = uint256(1) << 1;
|
|
185
|
+
|
|
186
|
+
// Operator tries to set permission on wildcard project (0).
|
|
187
|
+
uint8[] memory somePerms = new uint8[](1);
|
|
188
|
+
somePerms[0] = 5;
|
|
189
|
+
|
|
190
|
+
vm.prank(operator);
|
|
191
|
+
try PERMISSIONS.setPermissionsFor(
|
|
192
|
+
account, JBPermissionsData({operator: thirdParty, projectId: 0, permissionIds: somePerms})
|
|
193
|
+
) {
|
|
194
|
+
// Should not reach here.
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
wildcardSetBlocked++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// @notice Get the expected packed permissions for a triple.
|
|
202
|
+
function getExpected(address operator, address account, uint56 projectId) external view returns (uint256) {
|
|
203
|
+
bytes32 key = keccak256(abi.encodePacked(operator, account, projectId));
|
|
204
|
+
return expectedPacked[key];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// @title PermissionsInvariantTest
|
|
209
|
+
/// @notice Stateful invariant tests proving JBPermissions maintains consistency
|
|
210
|
+
/// through random set/revoke cycles.
|
|
211
|
+
contract PermissionsInvariantTest is Test {
|
|
212
|
+
PermissionsHandler handler;
|
|
213
|
+
|
|
214
|
+
function setUp() public {
|
|
215
|
+
handler = new PermissionsHandler();
|
|
216
|
+
targetContract(address(handler));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// @notice Packed permissions in storage always match what was last set.
|
|
220
|
+
function invariant_packedMatchesExpected() public view {
|
|
221
|
+
JBPermissions perms = handler.PERMISSIONS();
|
|
222
|
+
|
|
223
|
+
// Check all (operator, account, projectId) combinations.
|
|
224
|
+
for (uint256 o; o < 3; o++) {
|
|
225
|
+
for (uint256 a; a < 3; a++) {
|
|
226
|
+
for (uint256 p; p < 3; p++) {
|
|
227
|
+
address operator = handler.operators(o);
|
|
228
|
+
address account = handler.accounts(a);
|
|
229
|
+
uint56 projectId = handler.projectIds(p);
|
|
230
|
+
|
|
231
|
+
uint256 expected = handler.getExpected(operator, account, projectId);
|
|
232
|
+
uint256 actual = perms.permissionsOf(operator, account, projectId);
|
|
233
|
+
|
|
234
|
+
assertEq(actual, expected, "Packed permissions mismatch");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// @notice Bit 0 is never set in any stored permissions.
|
|
241
|
+
function invariant_bit0NeverSet() public view {
|
|
242
|
+
JBPermissions perms = handler.PERMISSIONS();
|
|
243
|
+
|
|
244
|
+
for (uint256 o; o < 3; o++) {
|
|
245
|
+
for (uint256 a; a < 3; a++) {
|
|
246
|
+
for (uint256 p; p < 3; p++) {
|
|
247
|
+
uint256 packed =
|
|
248
|
+
perms.permissionsOf(handler.operators(o), handler.accounts(a), handler.projectIds(p));
|
|
249
|
+
assertFalse((packed & 1) == 1, "Bit 0 should never be set");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// @notice ROOT forwarding is always blocked.
|
|
256
|
+
function invariant_rootForwardingAlwaysBlocked() public view {
|
|
257
|
+
if (handler.rootForwardAttempts() > 0) {
|
|
258
|
+
assertEq(
|
|
259
|
+
handler.rootForwardBlocked(),
|
|
260
|
+
handler.rootForwardAttempts(),
|
|
261
|
+
"All ROOT forwarding attempts must be blocked"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// @notice Wildcard permission setting by operators is always blocked.
|
|
267
|
+
function invariant_wildcardByOperatorAlwaysBlocked() public view {
|
|
268
|
+
if (handler.wildcardSetAttempts() > 0) {
|
|
269
|
+
assertEq(
|
|
270
|
+
handler.wildcardSetBlocked(),
|
|
271
|
+
handler.wildcardSetAttempts(),
|
|
272
|
+
"All wildcard-by-operator attempts must be blocked"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// @notice hasPermission returns true iff the bit is set (no false positives/negatives).
|
|
278
|
+
function invariant_hasPermissionMatchesBits() public view {
|
|
279
|
+
JBPermissions perms = handler.PERMISSIONS();
|
|
280
|
+
|
|
281
|
+
// Spot-check a few permission IDs across all triples.
|
|
282
|
+
uint256[5] memory checkIds = [uint256(1), 2, 5, 42, 255];
|
|
283
|
+
|
|
284
|
+
for (uint256 o; o < 3; o++) {
|
|
285
|
+
for (uint256 a; a < 3; a++) {
|
|
286
|
+
for (uint256 p; p < 3; p++) {
|
|
287
|
+
address operator = handler.operators(o);
|
|
288
|
+
address account = handler.accounts(a);
|
|
289
|
+
uint56 projectId = handler.projectIds(p);
|
|
290
|
+
|
|
291
|
+
uint256 packed = perms.permissionsOf(operator, account, projectId);
|
|
292
|
+
|
|
293
|
+
for (uint256 c; c < checkIds.length; c++) {
|
|
294
|
+
bool expected = ((packed >> checkIds[c]) & 1) == 1;
|
|
295
|
+
bool actual = perms.hasPermission(operator, account, projectId, checkIds[c], false, false);
|
|
296
|
+
assertEq(actual, expected, "hasPermission must match bit state");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// @title PermissionsBitPackingTest
|
|
305
|
+
/// @notice Formal property tests for JBPermissions bit-packing roundtrip
|
|
306
|
+
/// and hasPermissions batch logic.
|
|
307
|
+
contract PermissionsBitPackingTest is Test {
|
|
308
|
+
JBPermissions perms;
|
|
309
|
+
|
|
310
|
+
address account = makeAddr("account");
|
|
311
|
+
address operator = makeAddr("operator");
|
|
312
|
+
uint56 projectId = 7;
|
|
313
|
+
|
|
314
|
+
function setUp() public {
|
|
315
|
+
perms = new JBPermissions(address(0));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// @notice hasPermissions with an empty array returns true (vacuous truth).
|
|
319
|
+
function test_hasPermissions_emptyArray_returnsTrue() public view {
|
|
320
|
+
uint256[] memory empty = new uint256[](0);
|
|
321
|
+
bool result = perms.hasPermissions(operator, account, projectId, empty, false, false);
|
|
322
|
+
assertTrue(result, "Empty permission array should return true (vacuous truth)");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// @notice Fuzz: set N permissions, verify each bit is set and all others are not.
|
|
326
|
+
function testFuzz_bitPackingRoundtrip(uint8 id1, uint8 id2, uint8 id3) public {
|
|
327
|
+
// Bound to valid range (1-255).
|
|
328
|
+
id1 = uint8(bound(uint256(id1), 1, 255));
|
|
329
|
+
id2 = uint8(bound(uint256(id2), 1, 255));
|
|
330
|
+
id3 = uint8(bound(uint256(id3), 1, 255));
|
|
331
|
+
|
|
332
|
+
uint8[] memory ids = new uint8[](3);
|
|
333
|
+
ids[0] = id1;
|
|
334
|
+
ids[1] = id2;
|
|
335
|
+
ids[2] = id3;
|
|
336
|
+
|
|
337
|
+
vm.prank(account);
|
|
338
|
+
perms.setPermissionsFor(
|
|
339
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: ids})
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Each set ID should return true.
|
|
343
|
+
assertTrue(perms.hasPermission(operator, account, projectId, id1, false, false), "id1 should be set");
|
|
344
|
+
assertTrue(perms.hasPermission(operator, account, projectId, id2, false, false), "id2 should be set");
|
|
345
|
+
assertTrue(perms.hasPermission(operator, account, projectId, id3, false, false), "id3 should be set");
|
|
346
|
+
|
|
347
|
+
// Verify the packed value has exactly the right bits.
|
|
348
|
+
uint256 packed = perms.permissionsOf(operator, account, projectId);
|
|
349
|
+
uint256 expectedPacked = (uint256(1) << id1) | (uint256(1) << id2) | (uint256(1) << id3);
|
|
350
|
+
assertEq(packed, expectedPacked, "Packed value should exactly match OR of set bits");
|
|
351
|
+
|
|
352
|
+
// Bit 0 must not be set.
|
|
353
|
+
assertFalse((packed & 1) == 1, "Bit 0 must never be set");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/// @notice hasPermissions batch check: all permissions must be present.
|
|
357
|
+
function test_hasPermissions_batch_allRequired() public {
|
|
358
|
+
uint8[] memory ids = new uint8[](3);
|
|
359
|
+
ids[0] = 5;
|
|
360
|
+
ids[1] = 10;
|
|
361
|
+
ids[2] = 200;
|
|
362
|
+
|
|
363
|
+
vm.prank(account);
|
|
364
|
+
perms.setPermissionsFor(
|
|
365
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: ids})
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// All three set -> true.
|
|
369
|
+
uint256[] memory check3 = new uint256[](3);
|
|
370
|
+
check3[0] = 5;
|
|
371
|
+
check3[1] = 10;
|
|
372
|
+
check3[2] = 200;
|
|
373
|
+
assertTrue(perms.hasPermissions(operator, account, projectId, check3, false, false), "All three should pass");
|
|
374
|
+
|
|
375
|
+
// Missing one (15 not set) -> false.
|
|
376
|
+
uint256[] memory check4 = new uint256[](4);
|
|
377
|
+
check4[0] = 5;
|
|
378
|
+
check4[1] = 10;
|
|
379
|
+
check4[2] = 200;
|
|
380
|
+
check4[3] = 15;
|
|
381
|
+
assertFalse(
|
|
382
|
+
perms.hasPermissions(operator, account, projectId, check4, false, false),
|
|
383
|
+
"Missing permission 15 should fail batch check"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// @notice Event emission includes correct packed value.
|
|
388
|
+
function test_eventEmission_packedValue() public {
|
|
389
|
+
uint8[] memory ids = new uint8[](2);
|
|
390
|
+
ids[0] = 3;
|
|
391
|
+
ids[1] = 7;
|
|
392
|
+
|
|
393
|
+
uint256 expectedPacked = (uint256(1) << 3) | (uint256(1) << 7);
|
|
394
|
+
|
|
395
|
+
vm.expectEmit(true, true, true, true);
|
|
396
|
+
emit IJBPermissions.OperatorPermissionsSet(operator, account, projectId, ids, expectedPacked, account);
|
|
397
|
+
|
|
398
|
+
vm.prank(account);
|
|
399
|
+
perms.setPermissionsFor(
|
|
400
|
+
account, JBPermissionsData({operator: operator, projectId: projectId, permissionIds: ids})
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|