@bananapus/721-hook-v6 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +38 -11
- package/ARCHITECTURE.md +53 -99
- package/AUDIT_INSTRUCTIONS.md +84 -383
- package/CHANGELOG.md +71 -0
- package/README.md +79 -225
- package/RISKS.md +28 -11
- package/SKILLS.md +29 -296
- package/STYLE_GUIDE.md +57 -18
- package/USER_JOURNEYS.md +57 -501
- package/package.json +1 -1
- package/references/operations.md +28 -0
- package/references/runtime.md +32 -0
- package/script/Deploy.s.sol +5 -4
- package/src/JB721TiersHook.sol +1 -1
- package/src/JB721TiersHookDeployer.sol +1 -1
- package/src/JB721TiersHookProjectDeployer.sol +1 -1
- package/src/JB721TiersHookStore.sol +23 -17
- package/src/libraries/JB721Constants.sol +1 -1
- package/src/libraries/JB721TiersRulesetMetadataResolver.sol +1 -1
- package/src/libraries/JBBitmap.sol +1 -1
- package/src/libraries/JBIpfsDecoder.sol +1 -1
- package/src/structs/JB721Tier.sol +5 -11
- package/src/structs/JB721TierConfig.sol +5 -20
- package/src/structs/JB721TierConfigFlags.sol +26 -0
- package/src/structs/JB721TierFlags.sol +17 -0
- package/test/721HookAttacks.t.sol +22 -17
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +19 -14
- package/test/Fork.t.sol +69 -54
- package/test/TestAuditGaps.sol +73 -56
- package/test/TestSafeTransferReentrancy.t.sol +4 -4
- package/test/TestVotingUnitsLifecycle.t.sol +11 -11
- package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +10 -7
- package/test/audit/CodexSplitCreditsMismatch.t.sol +10 -7
- package/test/fork/ERC20CashOutFork.t.sol +37 -28
- package/test/fork/ERC20TierSplitFork.t.sol +28 -21
- package/test/fork/IssueTokensForSplitsFork.t.sol +10 -7
- package/test/invariants/handlers/TierLifecycleHandler.sol +10 -7
- package/test/invariants/handlers/TierStoreHandler.sol +10 -7
- package/test/regression/ProjectDeployerRulesets.t.sol +10 -7
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +6 -6
- package/test/unit/AuditFixes_Unit.t.sol +37 -28
- package/test/unit/adjustTier_Unit.t.sol +268 -202
- package/test/unit/getters_constructor_Unit.t.sol +20 -14
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +2 -2
- package/test/unit/pay_Unit.t.sol +1 -1
- package/CHANGE_LOG.md +0 -359
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -1,413 +1,114 @@
|
|
|
1
|
-
# Audit Instructions
|
|
1
|
+
# Audit Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This repo is the tiered ERC-721 hook system for Juicebox payments and NFT cash-outs. Audit it as a shared primitive used by many other repos.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Objective
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Find issues that:
|
|
8
|
+
- let users mint tiers more cheaply than intended
|
|
9
|
+
- over-mint, under-burn, or miscount reserves, credits, or supply
|
|
10
|
+
- route split funds incorrectly or let split paths distort token issuance
|
|
11
|
+
- let NFT cash-outs reclaim more value than intended
|
|
12
|
+
- corrupt shared store state across different hook instances
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|---------|-------|
|
|
11
|
-
| Solidity version | 0.8.28 |
|
|
12
|
-
| EVM target | cancun |
|
|
13
|
-
| Optimizer | enabled, 200 runs |
|
|
14
|
-
| via-IR | not enabled |
|
|
15
|
-
| Fuzz runs | 4,096 |
|
|
16
|
-
| Invariant runs | 1,024 (depth 100) |
|
|
14
|
+
## Scope
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
In scope:
|
|
17
|
+
- `src/JB721TiersHook.sol`
|
|
18
|
+
- `src/JB721TiersHookStore.sol`
|
|
19
|
+
- `src/JB721TiersHookDeployer.sol`
|
|
20
|
+
- `src/JB721TiersHookProjectDeployer.sol`
|
|
21
|
+
- `src/abstract/`
|
|
22
|
+
- `src/interfaces/`
|
|
23
|
+
- `src/libraries/`
|
|
24
|
+
- `src/structs/`
|
|
25
|
+
- deployment scripts in `script/`
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
This repo is depended on by Defifa, Croptop, Banny, Revnets, and omnichain deployers. Bugs here often have ecosystem-wide blast radius.
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
## System Model
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
| LOW | Missing `splitPercent` bounds validation in `recordAddTiers` | **Remediated** -- `SplitPercentExceedsBounds` check added at `JB721TiersHookStore.sol:866` |
|
|
29
|
-
| LOW | Implementation contract initializable | **Remediated** -- constructor now sets `_initialized = true` on the implementation |
|
|
30
|
-
| LOW | `setMetadata` uses non-standard sentinel for tokenUriResolver | Open (documented behavior) |
|
|
31
|
+
The hook can act as:
|
|
32
|
+
- a data hook for payment and cash-out accounting inputs
|
|
33
|
+
- a pay hook that mints NFTs
|
|
34
|
+
- a cash-out hook that burns NFTs and computes reclaim weight
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
Key moving parts:
|
|
37
|
+
- `JB721TiersHookStore` holds compact tier state
|
|
38
|
+
- hook instances read and mutate tier data through the store
|
|
39
|
+
- tier prices, discounts, reserves, credits, split percentages, and category order shape mint behavior
|
|
40
|
+
- optional token URI resolvers can override metadata generation
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
The most important design subtlety is that this repo affects both:
|
|
43
|
+
- NFT state
|
|
44
|
+
- core Juicebox accounting inputs and fulfillment order
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|-------|----------|-------------------|
|
|
38
|
-
| `JB721TiersHookStore_CantMintManually(uint256 tierId)` | JB721TiersHookStore | `recordMint` called with `isManualMint=true` on a tier with `allowOwnerMint=false` |
|
|
39
|
-
| `JB721TiersHookStore_CantRemoveTier(uint256 tierId)` | JB721TiersHookStore | `recordRemoveTierIds` called on a tier with `cannotBeRemoved=true` |
|
|
40
|
-
| `JB721TiersHookStore_DiscountPercentExceedsBounds(uint256 percent, uint256 limit)` | JB721TiersHookStore | `recordSetDiscountPercentOf` or `recordAddTiers` with `discountPercent > DISCOUNT_DENOMINATOR (200)` |
|
|
41
|
-
| `JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(uint256 percent, uint256 storedPercent)` | JB721TiersHookStore | `recordSetDiscountPercentOf` increases discount on a tier with `cannotIncreaseDiscountPercent=true` |
|
|
42
|
-
| `JB721TiersHookStore_InsufficientPendingReserves(uint256 count, uint256 numberOfPendingReserves)` | JB721TiersHookStore | `recordMintReservesFor` called with `count > pendingReserves` |
|
|
43
|
-
| `JB721TiersHookStore_InsufficientSupplyRemaining(uint256 tierId)` | JB721TiersHookStore | `recordMint` when `remainingSupply < pendingReserves` after decrement |
|
|
44
|
-
| `JB721TiersHookStore_InvalidCategorySortOrder(uint256 tierCategory, uint256 previousTierCategory)` | JB721TiersHookStore | `recordAddTiers` with tiers not in ascending category order |
|
|
45
|
-
| `JB721TiersHookStore_InvalidQuantity(uint256 quantity, uint256 limit)` | JB721TiersHookStore | `recordAddTiers` with `initialSupply >= _ONE_BILLION` |
|
|
46
|
-
| `JB721TiersHookStore_ManualMintingNotAllowed(uint256 tierId)` | JB721TiersHookStore | `recordMint` called as manual mint when `noNewTiersWithOwnerMinting` flag is set and tier allows it |
|
|
47
|
-
| `JB721TiersHookStore_MaxTiersExceeded(uint256 numberOfTiers, uint256 limit)` | JB721TiersHookStore | `recordAddTiers` would push `maxTierIdOf` above `type(uint16).max` (65,535) |
|
|
48
|
-
| `JB721TiersHookStore_PriceExceedsAmount(uint256 price, uint256 leftoverAmount)` | JB721TiersHookStore | `recordMint` when tier's (discounted) price exceeds remaining payment amount |
|
|
49
|
-
| `JB721TiersHookStore_ReserveFrequencyNotAllowed(uint256 tierId)` | JB721TiersHookStore | `recordAddTiers` with `reserveFrequency > 0` when `noNewTiersWithReserves` flag is set |
|
|
50
|
-
| `JB721TiersHookStore_SplitPercentExceedsBounds(uint256 percent, uint256 limit)` | JB721TiersHookStore | `recordAddTiers` with `splitPercent` exceeding bounds |
|
|
51
|
-
| `JB721TiersHookStore_TierRemoved(uint256 tierId)` | JB721TiersHookStore | `recordMint` or `recordSetDiscountPercentOf` called on a removed tier |
|
|
52
|
-
| `JB721TiersHookStore_UnrecognizedTier(uint256 tierId)` | JB721TiersHookStore | Any operation referencing a `tierId` that does not exist (> `maxTierIdOf` or never created) |
|
|
53
|
-
| `JB721TiersHookStore_VotingUnitsNotAllowed(uint256 tierId)` | JB721TiersHookStore | `recordAddTiers` with `votingUnits > 0` when `noNewTiersWithVotes` flag is set |
|
|
54
|
-
| `JB721TiersHookStore_ZeroInitialSupply(uint256 tierId)` | JB721TiersHookStore | `recordAddTiers` with `initialSupply == 0` |
|
|
55
|
-
| `JB721TiersHook_AlreadyInitialized(uint256 projectId)` | JB721TiersHook | `initialize()` called on a hook where `_initialized` is already true |
|
|
56
|
-
| `JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency)` | JB721TiersHook | Payment currency differs from tier pricing currency and no price feed is configured |
|
|
57
|
-
| `JB721TiersHook_InvalidPricingDecimals(uint256 decimals)` | JB721TiersHook | `initialize()` with `pricingDecimals > 18` |
|
|
58
|
-
| `JB721TiersHook_MintReserveNftsPaused()` | JB721TiersHook | `mintPendingReservesFor` called when `mintPendingReservesPaused` ruleset flag is active |
|
|
59
|
-
| `JB721TiersHook_NoProjectId()` | JB721TiersHook | `initialize()` called with `projectId == 0` |
|
|
60
|
-
| `JB721TiersHook_Overspending(uint256 leftoverAmount)` | JB721TiersHook | Payment has leftover after minting and `allowOverspending` metadata flag is false |
|
|
61
|
-
| `JB721TiersHook_TierTransfersPaused()` | JB721TiersHook | NFT transfer attempted on a tier with `transfersPausable=true` when `transfersPaused` ruleset flag is active |
|
|
62
|
-
| `JB721Hook_InvalidCashOut()` | JB721Hook | `afterCashOutRecordedWith` called by a non-terminal address |
|
|
63
|
-
| `JB721Hook_InvalidPay()` | JB721Hook | `afterPayRecordedWith` called by a non-terminal address |
|
|
64
|
-
| `JB721Hook_UnauthorizedToken(uint256 tokenId, address holder)` | JB721Hook | `afterCashOutRecordedWith` with a token ID not owned by the cash-out holder |
|
|
65
|
-
| `JB721Hook_UnexpectedTokenCashedOut()` | JB721Hook | `beforeCashOutRecordedWith` called with `cashOutCount > 0` (fungible tokens cannot be cashed out through this hook) |
|
|
46
|
+
That combination is why small-looking mistakes here often become ecosystem-wide economic bugs.
|
|
66
47
|
|
|
67
|
-
##
|
|
48
|
+
## Critical Invariants
|
|
68
49
|
|
|
69
|
-
|
|
50
|
+
1. Supply caps hold
|
|
51
|
+
No tier may mint beyond its configured total supply once purchases, owner mints, and pending reserves are all considered.
|
|
70
52
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| `JB721TiersHook` | ~790 | The hook itself. ERC-721 + data hook + pay hook + cash out hook. Handles payment processing, NFT minting, cash out burning, tier adjustment, reserve minting, discount setting, metadata, and split distribution. Delegates heavy logic to the library via DELEGATECALL. |
|
|
74
|
-
| `JB721TiersHookStore` | ~1230 | All tier state. Keyed by `msg.sender` (the hook address). Manages tier CRUD, supply tracking, reserve accounting, bitmap-based removal, sorted linked list, transfer balance tracking, voting units, discount enforcement. |
|
|
75
|
-
| `JB721TiersHookDeployer` | ~115 | Deploys hook clones (Solady `LibClone`). Optional deterministic addressing via salt. Atomic deploy + initialize + ownership transfer. Registers with `JBAddressRegistry`. |
|
|
76
|
-
| `JB721TiersHookProjectDeployer` | ~420 | Convenience: launches a project + hook in one transaction. Converts `JBPayDataHookRulesetConfig` to `JBRulesetConfig` with `useDataHookForPay: true` hardcoded. Also supports `launchRulesetsFor` and `queueRulesetsOf`. |
|
|
77
|
-
| `JB721TiersHookLib` (library) | ~634 | Extracted logic for EIP-170 compliance. Tier adjustments, split amount calculation, price normalization, weight adjustment, split fund distribution, token URI resolution. Called via DELEGATECALL from the hook. |
|
|
53
|
+
2. Reserve accounting is exact
|
|
54
|
+
Pending reserves must neither disappear nor inflate reclaim denominators beyond what the design intends.
|
|
78
55
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `ERC721` (abstract) -- Minimal ERC-721 with initializable name/symbol.
|
|
56
|
+
3. Split routing matches accounting
|
|
57
|
+
If part of a mint price is routed to splits, token issuance and treasury accounting must reflect only the intended project portion.
|
|
82
58
|
|
|
83
|
-
|
|
59
|
+
4. Cash-out weight is consistent
|
|
60
|
+
The reclaim value for NFTs must match documented tier economics and must not be manipulable through discounts, credits, cross-currency inputs, or reserve timing.
|
|
84
61
|
|
|
85
|
-
|
|
62
|
+
5. Shared store isolation
|
|
63
|
+
One hook instance must not corrupt or observe mutable state belonging to another project unexpectedly.
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-> beforePayRecordedWith() [JB721TiersHook, view]
|
|
90
|
-
-> JB721TiersHookLib.calculateSplitAmounts() -- per-tier split amounts from tier prices
|
|
91
|
-
-> JB721TiersHookLib.convertSplitAmounts() -- currency conversion if pricing != payment currency
|
|
92
|
-
-> JB721TiersHookLib.calculateWeight() -- reduce weight by split fraction
|
|
93
|
-
-> returns (weight, hookSpecifications[0] = {this, totalSplitAmount, splitMetadata})
|
|
65
|
+
6. Credit semantics remain bounded
|
|
66
|
+
Unused payment value that becomes credits must not let a user later mint tiers, trigger splits, or receive project-token issuance on terms they did not actually fund.
|
|
94
67
|
|
|
95
|
-
|
|
96
|
-
|
|
68
|
+
7. Resolver trust stays read-only unless explicitly intended
|
|
69
|
+
Token URI resolvers must not become an implicit control plane for mint, burn, or accounting behavior.
|
|
97
70
|
|
|
98
|
-
|
|
99
|
-
-> Terminal auth check (DIRECTORY.isTerminalOf)
|
|
100
|
-
-> _processPayment(context)
|
|
101
|
-
-> JB721TiersHookLib.normalizePaymentValue() -- convert to pricing currency
|
|
102
|
-
-> Combine pay credits (only if payer == beneficiary)
|
|
103
|
-
-> Decode metadata: (allowOverspending, tierIdsToMint)
|
|
104
|
-
-> _mintAll(amount, tierIds, beneficiary)
|
|
105
|
-
-> STORE.recordMint(amount, tierIds, false)
|
|
106
|
-
-- For each tier: check removed, check supply, apply discount, check price, decrement supply, check reserves
|
|
107
|
-
-> _mint(to, tokenId) for each [no onERC721Received callback]
|
|
108
|
-
-> Update pay credits
|
|
109
|
-
-> JB721TiersHookLib.distributeAll(context.hookMetadata) [if forwardedAmount > 0]
|
|
110
|
-
-> Pull ERC-20 from terminal (safeTransferFrom)
|
|
111
|
-
-> For each tier with splits: read splits from JBSplits, distribute via _sendPayoutToSplit
|
|
112
|
-
-> Leftover -> _addToBalance (back to project)
|
|
113
|
-
```
|
|
71
|
+
## Threat Model
|
|
114
72
|
|
|
115
|
-
|
|
73
|
+
Prioritize:
|
|
74
|
+
- overspending and leftover-credit edge cases
|
|
75
|
+
- cross-currency pricing with missing or stale feeds
|
|
76
|
+
- tier additions or adjustments with invalid sort order or percent bounds
|
|
77
|
+
- split hooks or terminal recipients that revert or partially fail
|
|
78
|
+
- data-hook and pay-hook interactions inside the same payment
|
|
116
79
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-> cashOutCount = STORE.cashOutWeightOf(tokenIds) -- sum of tier prices (original, not discounted)
|
|
122
|
-
-> totalSupply = STORE.totalCashOutWeight() -- all tiers, includes pending reserves
|
|
123
|
-
-> returns (cashOutTaxRate, cashOutCount, totalSupply, hookSpecs)
|
|
80
|
+
Especially high-value attacker profiles:
|
|
81
|
+
- a payer crafting metadata and tier selections to desync credits, split routing, and token issuance
|
|
82
|
+
- a project owner adjusting tiers between preview and execution windows
|
|
83
|
+
- a downstream app assuming tier cash-out weight tracks discounted price when the primitive uses different economics
|
|
124
84
|
|
|
125
|
-
|
|
85
|
+
## Hotspots
|
|
126
86
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
87
|
+
- `beforePayRecordedWith`, `afterPayRecordedWith`, and cash-out hooks
|
|
88
|
+
- credit handling when `payer != beneficiary`
|
|
89
|
+
- discount logic versus cash-out pricing
|
|
90
|
+
- pending reserve minting and denominator logic
|
|
91
|
+
- `splitPercent` handling and hook distribution fallback behavior
|
|
92
|
+
- deployers that transfer ownership or queue rulesets around the hook
|
|
132
93
|
|
|
133
|
-
|
|
94
|
+
## Sequences Worth Replaying
|
|
134
95
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
-> STORE.recordAddTiers(tiersToAdd) -- sorted insert into linked list
|
|
141
|
-
-> SPLITS.setSplitGroupsOf() for tiers with splits configured
|
|
142
|
-
```
|
|
96
|
+
1. Cross-currency payment where prices are missing, stale, or intentionally asymmetric.
|
|
97
|
+
2. Payment with leftover value that becomes credits, then a second payment from a different payer/beneficiary arrangement.
|
|
98
|
+
3. Tier purchases with split routing enabled, especially when split hooks or downstream terminals fail.
|
|
99
|
+
4. Reserve-heavy tiers followed by NFT cash-out before pending reserves are minted.
|
|
100
|
+
5. Tier adjustment or discount updates around active minting and cash-out windows.
|
|
143
101
|
|
|
144
|
-
|
|
102
|
+
## Build And Verification
|
|
145
103
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-- Checks pendingReserves >= count
|
|
151
|
-
-- Increments numberOfReservesMintedFor
|
|
152
|
-
-- Decrements remainingSupply
|
|
153
|
-
-> STORE.reserveBeneficiaryOf(hook, tierId) -- tier-specific or default
|
|
154
|
-
-> _mint(to, tokenId) for each
|
|
155
|
-
```
|
|
104
|
+
Standard workflow:
|
|
105
|
+
- `npm install`
|
|
106
|
+
- `forge build`
|
|
107
|
+
- `forge test`
|
|
156
108
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Tiers are stored individually in `_storedTierOf[hook][tierId]` as `JBStored721Tier` structs. The sorted iteration order is maintained by:
|
|
162
|
-
|
|
163
|
-
- `_tierIdAfter[hook][tierId]` -- next tier in sorted order (0 means tierId+1 is next)
|
|
164
|
-
- `_tierIdAfter[hook][0]` -- first tier in sorted order
|
|
165
|
-
- `_lastTrackedSortedTierIdOf[hook]` -- last tier if explicitly tracked (else `maxTierIdOf`)
|
|
166
|
-
- `_startingTierIdOfCategory[hook][category]` -- first tier ID for a given category
|
|
167
|
-
|
|
168
|
-
New tiers are always assigned incrementing IDs (`maxTierIdOf + 1, +2, ...`) regardless of category. The linked list is updated to insert them at the correct sorted position.
|
|
169
|
-
|
|
170
|
-
### Tier Removal Bitmap
|
|
171
|
-
|
|
172
|
-
Tiers are never deleted from storage. Removal is tracked in `_removedTiersBitmapWordOf[hook]` using the `JBBitmap` library. Each word stores 256 tier removal flags. Removed tiers are skipped during sorted iteration but their data persists for:
|
|
173
|
-
- Cash out weight calculation (`totalCashOutWeight` iterates by maxTierIdOf, not by sorted list)
|
|
174
|
-
- Existing NFT metadata resolution
|
|
175
|
-
- Reserve minting (reserves can still be minted from removed tiers)
|
|
176
|
-
|
|
177
|
-
### Pay Credits
|
|
178
|
-
|
|
179
|
-
`payCreditsOf[beneficiary]` in the hook contract (not the store). Tracks overpayment in the pricing currency denomination. Only combined with incoming payment when `payer == beneficiary`.
|
|
180
|
-
|
|
181
|
-
### Token ID Encoding
|
|
182
|
-
|
|
183
|
-
`tokenId = tierId * 1_000_000_000 + tokenNumber`
|
|
184
|
-
|
|
185
|
-
Where `tokenNumber` is `initialSupply - remainingSupply` at mint time. This means:
|
|
186
|
-
- `tierIdOfToken(tokenId) = tokenId / 1_000_000_000`
|
|
187
|
-
- Max supply per tier: 999,999,999 (enforced as `_ONE_BILLION - 1`)
|
|
188
|
-
- Max tier ID: 65,535 (`type(uint16).max`)
|
|
189
|
-
|
|
190
|
-
### Split Group ID Encoding
|
|
191
|
-
|
|
192
|
-
Split groups are stored in `JBSplits` with a composite group ID:
|
|
193
|
-
```
|
|
194
|
-
groupId = uint256(uint160(hookAddress)) | (uint256(tierId) << 160)
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Key Constants
|
|
198
|
-
|
|
199
|
-
| Constant | Value | Where |
|
|
200
|
-
|----------|-------|-------|
|
|
201
|
-
| `DISCOUNT_DENOMINATOR` | 200 | `JB721Constants.sol` -- 200 = 100% discount, NOT 100 |
|
|
202
|
-
| `SPLITS_TOTAL_PERCENT` | 1,000,000,000 | `JBConstants` -- `splitPercent` is out of 1e9 |
|
|
203
|
-
| `_ONE_BILLION` | 1,000,000,000 | `JB721TiersHookStore` -- token ID namespace per tier |
|
|
204
|
-
| Max tier ID | 65,535 | `type(uint16).max` enforced in `recordAddTiers` |
|
|
205
|
-
| Max supply per tier | 999,999,999 | `_ONE_BILLION - 1` enforced in `recordAddTiers` |
|
|
206
|
-
|
|
207
|
-
## Gotchas -- Things That Trip Up Auditors
|
|
208
|
-
|
|
209
|
-
1. **Discount denominator is 200, not 100.** A `discountPercent` of 100 means 50% off. A `discountPercent` of 200 means 100% off (free). The formula: `effectivePrice = price - mulDiv(price, discountPercent, 200)`.
|
|
210
|
-
|
|
211
|
-
2. **Cash out weight uses original price, not discounted price.** `cashOutWeightOf` and `totalCashOutWeight` both use `storedTier.price` directly. If a tier has `discountPercent = 200` (free), NFTs minted for free still carry full cash-out weight. This is by design but creates an arbitrage vector if discount can be increased (see R-2 in RISKS.md).
|
|
212
|
-
|
|
213
|
-
3. **Category sort order is enforced on-chain.** `recordAddTiers` reverts with `InvalidCategorySortOrder` if tiers are not passed in ascending category order. This is a common integration footgun.
|
|
214
|
-
|
|
215
|
-
4. **Tier removal is soft.** `recordRemoveTierIds` only sets a bitmap flag. The stored tier data, cash-out weight, and reserve accounting all persist. `totalCashOutWeight` iterates by `maxTierIdOf`, not by the sorted list, so removed tier NFTs retain their cash-out value.
|
|
216
|
-
|
|
217
|
-
5. **Pay credits accrue to the beneficiary, not the payer.** When `payer != beneficiary`, the payer's existing credits are NOT applied to the mint. The leftover from the payment becomes the beneficiary's credit. This is documented but non-obvious.
|
|
218
|
-
|
|
219
|
-
6. **`splitPercent` is out of 1,000,000,000 (1e9), not 10,000.** A `splitPercent` of 500,000,000 means 50% of the tier's effective (discounted) price is routed to splits.
|
|
220
|
-
|
|
221
|
-
7. **`useReserveBeneficiaryAsDefault` overwrites the global default.** Adding a tier with this flag silently redirects reserve mints for ALL existing tiers that rely on the default beneficiary.
|
|
222
|
-
|
|
223
|
-
8. **No `ReentrancyGuard`.** The hook relies on state-before-interaction ordering and try-catch wrapping. All `STORE.record*` calls and `_mint()` calls happen before any untrusted external calls (split distribution). All external calls in `_sendPayoutToSplit` are wrapped in try-catch so a reverting recipient cannot block payments.
|
|
224
|
-
|
|
225
|
-
9. **`_mint()` is used, not `_safeMint()`.** The `onERC721Received` callback is NOT triggered during minting. This prevents mint-time DoS but means contracts that expect the callback won't detect incoming NFTs.
|
|
226
|
-
|
|
227
|
-
10. **`recordMint` decrements supply BEFORE checking reserves.** The remaining supply check `remainingSupply < _numberOfPendingReservesFor(...)` happens after the decrement. This is intentional -- the post-mint state correctly reflects the new non-reserve mint that may have created a new pending reserve.
|
|
228
|
-
|
|
229
|
-
11. **`totalCashOutWeight` includes pending reserves.** This dilutes cash-out value for existing holders by counting reserves that haven't been minted yet. By design -- prevents early cashers from extracting more than their fair share.
|
|
230
|
-
|
|
231
|
-
12. **`beforePayRecordedWith` computes split amounts in the pricing currency, then converts.** The split amount forwarded to the hook is in the payment token denomination. If the price feed has significant spread, the conversion can over/under-estimate.
|
|
232
|
-
|
|
233
|
-
## Priority Audit Areas
|
|
234
|
-
|
|
235
|
-
Audit in this order:
|
|
236
|
-
|
|
237
|
-
### 1. Split Distribution (Highest Risk)
|
|
238
|
-
|
|
239
|
-
The split distribution path in `JB721TiersHookLib.distributeAll()` is the largest attack surface:
|
|
240
|
-
|
|
241
|
-
- External calls to untrusted split hooks (`processSplitWith{value}`)
|
|
242
|
-
- External calls to arbitrary terminals (`terminal.pay()`, `terminal.addToBalanceOf()`)
|
|
243
|
-
- External calls to arbitrary beneficiary addresses (`.call{value}`)
|
|
244
|
-
- ERC-20 token transfers and approvals before external calls
|
|
245
|
-
- No `ReentrancyGuard` -- relies on state ordering and try-catch wrapping
|
|
246
|
-
|
|
247
|
-
All external calls in `_sendPayoutToSplit` are wrapped in try-catch so a single reverting recipient cannot brick all payments to the project. Behavior differs by token type:
|
|
248
|
-
- **Native token (ETH):** Split hooks, terminal calls, and beneficiary sends are wrapped in try-catch. On revert, ETH stays with the caller and the function returns `false`, routing the amount to the project's balance via `_addToBalance`.
|
|
249
|
-
- **ERC-20 split hooks:** Tokens are transferred via `safeTransfer` before the hook callback. The callback is wrapped in try-catch, but the function always returns `true` regardless of callback success — because the tokens have already left the contract, returning `false` would cause double-spend accounting in the leftover calculation.
|
|
250
|
-
- **ERC-20 terminal calls:** `forceApprove` is called before the terminal call. On failure, the approval is reset to zero to prevent dangling approvals, and the function returns `false`.
|
|
251
|
-
|
|
252
|
-
Verify that:
|
|
253
|
-
- State is fully settled before any external call in the distribution loop
|
|
254
|
-
- A reentering call through `terminal.pay()` cannot corrupt hook state
|
|
255
|
-
- `leftoverAmount` accounting is correct when `_sendPayoutToSplit` returns false
|
|
256
|
-
- ERC-20 `forceApprove` followed by external call cannot be exploited (approval not consumed -> leftover approval)
|
|
257
|
-
- The ERC-20 split hook path correctly returns `true` after `safeTransfer` regardless of callback outcome (prevents double-spend via leftover miscounting)
|
|
258
|
-
|
|
259
|
-
### 2. Discount / Cash Out Weight Interaction
|
|
260
|
-
|
|
261
|
-
The discount system creates a price asymmetry:
|
|
262
|
-
- Mint price: `price - mulDiv(price, discountPercent, 200)` (can be zero)
|
|
263
|
-
- Cash out weight: `price` (always original, never discounted)
|
|
264
|
-
|
|
265
|
-
Verify that:
|
|
266
|
-
- `cannotIncreaseDiscountPercent` is correctly enforced in `recordSetDiscountPercentOf`
|
|
267
|
-
- Split amounts use the discounted price (they do -- `calculateSplitAmounts` applies discount)
|
|
268
|
-
- There is no path to mint at discounted price and cash out at original weight without the owner explicitly enabling it
|
|
269
|
-
|
|
270
|
-
### 3. Reserve Accounting
|
|
271
|
-
|
|
272
|
-
Reserve mints interact with supply tracking:
|
|
273
|
-
- `_numberOfPendingReservesFor` uses `ceil(nonReserveMints / reserveFrequency) - reservesMinted`
|
|
274
|
-
- `recordMint` checks `remainingSupply < pendingReserves` after decrementing
|
|
275
|
-
- Reserve mints decrement `remainingSupply` and increment `numberOfReservesMintedFor`
|
|
276
|
-
|
|
277
|
-
Verify that:
|
|
278
|
-
- A paid mint cannot steal the last slot reserved for a pending reserve
|
|
279
|
-
- `_numberOfPendingReservesFor` never returns more than `remainingSupply`
|
|
280
|
-
- The rounding-up in pending reserve calculation is correct and consistent
|
|
281
|
-
- Changing `defaultReserveBeneficiaryOf` cannot create ghost reserves or destroy legitimate ones
|
|
282
|
-
|
|
283
|
-
### 4. Cross-Currency Price Normalization
|
|
284
|
-
|
|
285
|
-
Two conversion points:
|
|
286
|
-
- `normalizePaymentValue` -- converts payment amount to pricing currency for tier price comparison
|
|
287
|
-
- `convertSplitAmounts` -- converts split amounts from pricing currency to payment token denomination
|
|
288
|
-
|
|
289
|
-
Verify that:
|
|
290
|
-
- When `address(prices) == address(0)` and currencies differ, `normalizePaymentValue` returns `(0, false)` and the hook skips minting (no silent fund loss)
|
|
291
|
-
- A reverting price feed blocks payments but does not lose funds
|
|
292
|
-
- Rounding through the conversion chain (normalize -> split calc -> convert back) does not systematically favor the attacker
|
|
293
|
-
- The ratio used in `convertSplitAmounts` is the inverse of what `normalizePaymentValue` uses (it should be -- verify)
|
|
294
|
-
|
|
295
|
-
### 5. Initialization and Clone Security
|
|
296
|
-
|
|
297
|
-
`JB721TiersHookDeployer` creates minimal proxy clones:
|
|
298
|
-
- `initialize()` is guarded by an `_initialized` bool flag
|
|
299
|
-
- Ownership is transferred to `_msgSender()` inside `initialize`, then to the deployer caller in `deployHookFor`
|
|
300
|
-
|
|
301
|
-
Verify that:
|
|
302
|
-
- The implementation contract's constructor sets `_initialized = true`, preventing initialization.
|
|
303
|
-
- Deterministic salt derivation (`keccak256(abi.encode(_msgSender(), salt))`) prevents cross-deployer address collision
|
|
304
|
-
- Front-running `deployHookFor` cannot hijack ownership
|
|
305
|
-
|
|
306
|
-
### 6. Linked List Integrity
|
|
307
|
-
|
|
308
|
-
Tier sorting is maintained by `_tierIdAfter` mappings:
|
|
309
|
-
- `recordAddTiers` inserts new tiers into the sorted list
|
|
310
|
-
- `cleanTiers` removes gaps from the sorted list
|
|
311
|
-
- `_nextSortedTierIdOf` defaults to `id + 1` when no explicit next is stored
|
|
312
|
-
|
|
313
|
-
Verify that:
|
|
314
|
-
- Adding tiers to an existing set preserves the correct sort order
|
|
315
|
-
- Removing and re-adding tiers does not corrupt the linked list
|
|
316
|
-
- `cleanTiers` (permissionless) cannot be used to manipulate tier ordering in a way that affects minting or pricing
|
|
317
|
-
|
|
318
|
-
## Invariants
|
|
319
|
-
|
|
320
|
-
These must hold. If you can break any, it's a finding:
|
|
321
|
-
|
|
322
|
-
1. **Supply cap**: For every tier, `initialSupply - remainingSupply` (minted count) never exceeds `initialSupply`.
|
|
323
|
-
2. **Reserve protection**: After any `recordMint`, `remainingSupply >= numberOfPendingReserves` for that tier.
|
|
324
|
-
3. **Token ID uniqueness**: No two distinct mints produce the same `tokenId` (guaranteed by `initialSupply - --remainingSupply` pattern).
|
|
325
|
-
4. **Cash out weight conservation**: `totalCashOutWeight` equals the sum of `price * (mintedCount + pendingReserves)` across all tiers.
|
|
326
|
-
5. **Balance tracking**: `sum(tierBalanceOf[hook][owner][tierId])` across all owners equals `initialSupply - remainingSupply - burned` for each tier.
|
|
327
|
-
6. **Credit conservation**: Pay credits increase by leftover after minting, decrease by amount used for minting. Never negative.
|
|
328
|
-
7. **Linked list completeness**: Iterating from `_firstSortedTierIdOf(hook, 0)` via `_nextSortedTierIdOf` visits every non-removed tier exactly once.
|
|
329
|
-
8. **Discount bound**: `discountPercent <= DISCOUNT_DENOMINATOR (200)` for every stored tier.
|
|
330
|
-
9. **Removal idempotency**: Removing an already-removed tier is a no-op (bitmap set is idempotent).
|
|
331
|
-
10. **NFT supply cap**: Minted count per tier never exceeds `initialSupply` (same as invariant 1, but auditors should verify the `_ONE_BILLION - 1` cap prevents token ID overflow into the next tier).
|
|
332
|
-
|
|
333
|
-
## Anti-Patterns to Hunt
|
|
334
|
-
|
|
335
|
-
| Pattern | Where to Look | Why It's Dangerous |
|
|
336
|
-
|---------|--------------|-------------------|
|
|
337
|
-
| DELEGATECALL from hook to library | `JB721TiersHook` → `JB721TiersHookLib` | Library executes in the hook's storage context. A subtle mismatch in storage layout assumptions could corrupt state. |
|
|
338
|
-
| `safeTransfer` before callback | `_sendPayoutToSplit` ERC-20 path | Tokens leave the contract before the hook callback. The function returns `true` regardless of callback success to prevent double-spend in leftover accounting. |
|
|
339
|
-
| `forceApprove` + external call | `_sendPayoutToSplit` terminal path | If the external call fails, the approval is reset to zero. But between `forceApprove` and the failure, the approval exists. Can an attacker exploit this window? |
|
|
340
|
-
| `mulDiv` rounding in price normalization | `normalizePaymentValue`, `convertSplitAmounts` | Rounding through the conversion chain (normalize → calculate splits → convert back) can compound. Verify rounding favors the protocol. |
|
|
341
|
-
| Bitmap-based removal with iteration by maxTierIdOf | `totalCashOutWeight`, `cleanTiers` | `totalCashOutWeight` iterates up to `maxTierIdOf`, not by sorted list. If many tiers are added and removed, gas cost grows unboundedly. |
|
|
342
|
-
| Clone initialization guard | `JB721TiersHookDeployer` | `initialize()` is guarded by an `_initialized` bool flag. The implementation contract's constructor sets `_initialized = true`. Verify clones cannot be re-initialized. |
|
|
343
|
-
| `_mint` instead of `_safeMint` | `JB721TiersHook` | No `onERC721Received` callback. Prevents mint-time DoS but contracts won't detect incoming NFTs. |
|
|
344
|
-
| Token ID overflow at tier boundary | `_generateTokenId` | `tokenId = tierId * 1_000_000_000 + tokenNumber`. If `tokenNumber` reaches `_ONE_BILLION`, it overflows into the next tier's namespace. Supply cap enforcement (`_ONE_BILLION - 1`) prevents this -- verify the enforcement is complete. |
|
|
345
|
-
|
|
346
|
-
## Testing Setup
|
|
347
|
-
|
|
348
|
-
```bash
|
|
349
|
-
cd nana-721-hook-v6
|
|
350
|
-
npm install
|
|
351
|
-
forge build
|
|
352
|
-
forge test
|
|
353
|
-
|
|
354
|
-
# Run with high verbosity
|
|
355
|
-
forge test -vvvv --match-test testExploitName
|
|
356
|
-
|
|
357
|
-
# Write a PoC
|
|
358
|
-
forge test --match-path test/audit/ExploitPoC.t.sol -vvv
|
|
359
|
-
|
|
360
|
-
# Run invariant tests
|
|
361
|
-
forge test --match-contract Invariant
|
|
362
|
-
|
|
363
|
-
# Gas analysis
|
|
364
|
-
forge test --gas-report
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Existing Test Coverage
|
|
368
|
-
|
|
369
|
-
| Category | Files | Coverage |
|
|
370
|
-
|----------|------:|---------|
|
|
371
|
-
| Unit tests | 13 | adjustTier, deployer, getters/constructor, mintFor/mintReservesFor, pay, redeem, tierSplitRouting, splitHookDistribution, JBBitmap, JBIpfsDecoder, pay_CrossCurrency, JB721TiersRulesetMetadataResolver, TierSupplyReserveCheck |
|
|
372
|
-
| Invariant tests | 2 + 2 handlers | TierLifecycleInvariant (6), TieredHookStoreInvariant (3) |
|
|
373
|
-
| Attack tests | 1 | 10 adversarial scenarios |
|
|
374
|
-
| Regression tests | 6 | BrokenTerminalDoesNotDos, CacheTierLookup, ProjectDeployerRulesets, ReserveBeneficiaryOverwrite, SplitDistributionBugs, SplitNoBeneficiary |
|
|
375
|
-
| E2E tests | 1 | Full lifecycle |
|
|
376
|
-
| Fork tests | 3 | ERC20CashOutFork, ERC20TierSplitFork, IssueTokensForSplitsFork |
|
|
377
|
-
| Supply edge cases | 1 | M6 -- 4 targeted tests |
|
|
378
|
-
| Reentrancy tests | 1 | TestSafeTransferReentrancy -- safeTransfer reentrancy scenarios |
|
|
379
|
-
| Voting units tests | 1 | TestVotingUnitsLifecycle -- voting power through mint/burn/transfer |
|
|
380
|
-
|
|
381
|
-
### Coverage Gaps
|
|
382
|
-
|
|
383
|
-
1. No gas limit test for operations with hundreds of tiers.
|
|
384
|
-
2. No test for malicious/reverting token URI resolver.
|
|
385
|
-
3. No test for `initialize()` front-running on deterministic clones.
|
|
386
|
-
4. No fuzz test for discount percent edge cases with very small prices.
|
|
387
|
-
|
|
388
|
-
## How to Report Findings
|
|
389
|
-
|
|
390
|
-
For each finding:
|
|
391
|
-
|
|
392
|
-
1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
|
|
393
|
-
2. **Affected contract(s)** -- exact file path and line numbers
|
|
394
|
-
3. **Description** -- what's wrong, in plain language
|
|
395
|
-
4. **Trigger sequence** -- step-by-step, minimal steps to reproduce
|
|
396
|
-
5. **Impact** -- what an attacker gains, what a user loses (with numbers if possible)
|
|
397
|
-
6. **Proof** -- code trace showing the exact execution path, or a Foundry test
|
|
398
|
-
7. **Fix** -- minimal code change that resolves the issue
|
|
399
|
-
|
|
400
|
-
**Severity guide:**
|
|
401
|
-
- **CRITICAL**: Direct fund loss, permanent DoS, or broken core invariant. Exploitable with no preconditions.
|
|
402
|
-
- **HIGH**: Conditional fund loss, privilege escalation, or broken invariant. Requires specific but realistic setup.
|
|
403
|
-
- **MEDIUM**: Value leakage, griefing with cost to attacker, incorrect accounting, degraded functionality.
|
|
404
|
-
- **LOW**: Informational, cosmetic, edge-case-only with no material impact.
|
|
405
|
-
|
|
406
|
-
**Before reporting -- verify it's not a false positive:**
|
|
407
|
-
- Is the "bug" already documented in [RISKS.md](./RISKS.md)?
|
|
408
|
-
- Does cash out weight using original price (not discounted) look intentional? (It is.)
|
|
409
|
-
- Does `totalCashOutWeight` including pending reserves look wrong? (It's by design.)
|
|
410
|
-
- Is `DISCOUNT_DENOMINATOR = 200` surprising but correct? (It is.)
|
|
411
|
-
- Does the store's `msg.sender`-keyed trust model handle the case? (The store trusts the hook.)
|
|
412
|
-
- Is the economic attack profitable after the core protocol's 2.5% fee on cash outs?
|
|
109
|
+
Current tests emphasize:
|
|
110
|
+
- audit and regression fixes around split accounting and cross-currency behavior
|
|
111
|
+
- invariants on tier lifecycle and store state
|
|
112
|
+
- fork coverage for ERC-20 cash-out and tier split routes
|
|
413
113
|
|
|
114
|
+
High-value findings in this repo tend to become repeatable vulnerabilities in downstream repos, so favor proofs that show the primitive itself returning or recording the wrong value.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This file describes the verified change from `nana-721-hook-v5` to the current `nana-721-hook-v6` repo.
|
|
6
|
+
|
|
7
|
+
## Current v6 surface
|
|
8
|
+
|
|
9
|
+
- `JB721TiersHook`
|
|
10
|
+
- `JB721TiersHookStore`
|
|
11
|
+
- `JB721TiersHookDeployer`
|
|
12
|
+
- `JB721TiersHookProjectDeployer`
|
|
13
|
+
- `JB721TiersHookLib`
|
|
14
|
+
|
|
15
|
+
## Summary
|
|
16
|
+
|
|
17
|
+
- v6 adds tier-level split routing. `JB721TierConfig` and the surrounding minting logic now support `splitPercent` and `splits`.
|
|
18
|
+
- Collection metadata is more flexible than in v5. The hook can update name and symbol through the v6 metadata flow.
|
|
19
|
+
- Pricing context is cleaner. The hook no longer exposes prices through the old return shape, and pricing assumptions should be rebuilt from the current interfaces.
|
|
20
|
+
- The repo now carries a dedicated helper library to keep the hook surface manageable and to support the larger v6 feature set.
|
|
21
|
+
- The repo was upgraded from the v5 Solidity baseline to `0.8.28`.
|
|
22
|
+
|
|
23
|
+
## Verified deltas
|
|
24
|
+
|
|
25
|
+
- `IJB721TiersHook.pricingContext()` changed from a three-value return to `(currency, decimals)`.
|
|
26
|
+
- `IJB721TiersHook.PRICES()` is now an explicit getter instead of being bundled into `pricingContext()`.
|
|
27
|
+
- `IJB721TiersHook.SPLITS()` is new and matches the new tier-splits feature.
|
|
28
|
+
- `IJB721TiersHook.setMetadata(...)` now takes `name` and `symbol` before the URI fields.
|
|
29
|
+
- The interface gained new event surface around split payout failure handling and collection metadata updates.
|
|
30
|
+
|
|
31
|
+
## Breaking ABI changes
|
|
32
|
+
|
|
33
|
+
- `pricingContext()` return shape changed.
|
|
34
|
+
- `setMetadata(...)` argument order changed and now includes `name` and `symbol`.
|
|
35
|
+
- `JB721TierConfig` gained `cantBuyWithCredits`, `splitPercent`, and `splits`. Boolean flags (`allowOwnerMint`, `useReserveBeneficiaryAsDefault`, `transfersPausable`, `useVotingUnits`, `cantBeRemoved`, `cantIncreaseDiscountPercent`, `cantBuyWithCredits`) are nested in a `flags` field of type `JB721TierConfigFlags`.
|
|
36
|
+
- `JB721Tier` boolean flags (`allowOwnerMint`, `transfersPausable`, `cantBeRemoved`, `cantIncreaseDiscountPercent`, `cantBuyWithCredits`) are nested in a `flags` field of type `JB721TierFlags`.
|
|
37
|
+
- `JBStored721Tier` replaced packed `votingUnits` storage with `splitPercent` in the stored struct layout.
|
|
38
|
+
- `SPLITS()` and `PRICES()` are explicit interface getters.
|
|
39
|
+
|
|
40
|
+
## Indexer impact
|
|
41
|
+
|
|
42
|
+
- New events: `AddToBalanceReverted`, `SetName`, `SetSymbol`, `SplitPayoutReverted`.
|
|
43
|
+
- Tier config decoding changed because `JB721TierConfig` is no longer v5-compatible.
|
|
44
|
+
- Collection metadata can now change after deployment, so one-time indexing of `name` and `symbol` is no longer sufficient.
|
|
45
|
+
|
|
46
|
+
## Migration notes
|
|
47
|
+
|
|
48
|
+
- Rebuild integrations around the current `IJB721TiersHook` and related structs. This is not a selector-stable upgrade.
|
|
49
|
+
- Any indexer or frontend that decoded tier config data must account for tier splits.
|
|
50
|
+
- If you relied on v5 pricing-context return shapes or older metadata argument ordering, update those assumptions before shipping.
|
|
51
|
+
|
|
52
|
+
## ABI appendix
|
|
53
|
+
|
|
54
|
+
- Added functions
|
|
55
|
+
- `PRICES()`
|
|
56
|
+
- `SPLITS()`
|
|
57
|
+
- Changed functions
|
|
58
|
+
- `pricingContext()`
|
|
59
|
+
- `setMetadata(...)`
|
|
60
|
+
- Added events
|
|
61
|
+
- `AddToBalanceReverted`
|
|
62
|
+
- `SetName`
|
|
63
|
+
- `SetSymbol`
|
|
64
|
+
- `SplitPayoutReverted`
|
|
65
|
+
- Changed structs
|
|
66
|
+
- `JB721TierConfig` (boolean flags moved to nested `JB721TierConfigFlags flags`)
|
|
67
|
+
- `JB721Tier` (boolean flags moved to nested `JB721TierFlags flags`)
|
|
68
|
+
- `JBStored721Tier`
|
|
69
|
+
- Added structs
|
|
70
|
+
- `JB721TierConfigFlags`
|
|
71
|
+
- `JB721TierFlags`
|