@bananapus/721-hook-v6 0.0.34 → 0.0.36

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.
Files changed (36) hide show
  1. package/ADMINISTRATION.md +62 -164
  2. package/ARCHITECTURE.md +59 -44
  3. package/AUDIT_INSTRUCTIONS.md +35 -32
  4. package/README.md +22 -3
  5. package/RISKS.md +7 -1
  6. package/SKILLS.md +8 -2
  7. package/USER_JOURNEYS.md +144 -49
  8. package/foundry.toml +2 -0
  9. package/package.json +1 -1
  10. package/references/operations.md +7 -3
  11. package/references/runtime.md +5 -4
  12. package/src/JB721TiersHook.sol +6 -6
  13. package/src/JB721TiersHookProjectDeployer.sol +0 -1
  14. package/src/JB721TiersHookStore.sol +1 -2
  15. package/src/abstract/JB721Hook.sol +0 -1
  16. package/src/interfaces/IJB721TiersHook.sol +0 -2
  17. package/src/interfaces/IJB721TiersHookStore.sol +1 -1
  18. package/src/libraries/JB721Constants.sol +0 -1
  19. package/src/structs/JB721InitTiersConfig.sol +0 -1
  20. package/src/structs/JB721Tier.sol +0 -2
  21. package/src/structs/JB721TierConfig.sol +0 -2
  22. package/src/structs/JB721TierConfigFlags.sol +0 -1
  23. package/src/structs/JB721TierFlags.sol +0 -1
  24. package/src/structs/JB721TiersHookFlags.sol +0 -1
  25. package/src/structs/JB721TiersMintReservesConfig.sol +0 -1
  26. package/src/structs/JB721TiersRulesetMetadata.sol +0 -1
  27. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +0 -1
  28. package/src/structs/JBBitmapWord.sol +0 -1
  29. package/src/structs/JBDeploy721TiersHookConfig.sol +0 -1
  30. package/src/structs/JBLaunchProjectConfig.sol +0 -1
  31. package/src/structs/JBLaunchRulesetsConfig.sol +0 -1
  32. package/src/structs/JBPayDataHookRulesetConfig.sol +0 -1
  33. package/src/structs/JBPayDataHookRulesetMetadata.sol +0 -1
  34. package/src/structs/JBQueueRulesetsConfig.sol +0 -1
  35. package/src/structs/JBStored721Tier.sol +0 -1
  36. package/test/TestCheckpoints.t.sol +16 -4
package/SKILLS.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Use This File For
4
4
 
5
5
  - Use this file when the task involves tiered NFT issuance, reserve minting, voting units, tier splits, or token URI resolver integration for Juicebox projects.
6
- - Start here, then open the hook, store, deployer, or tests that own the exact behavior you are changing.
6
+ - Start here, then decide whether the bug is in hook runtime logic, store accounting, deployer initialization, or downstream token-URI resolution. This repo spans all four and they are easy to conflate.
7
7
 
8
8
  ## Read This Next
9
9
 
@@ -14,7 +14,8 @@
14
14
  | Tier storage and accounting | [`src/JB721TiersHookStore.sol`](./src/JB721TiersHookStore.sol) |
15
15
  | Deployment or project launch helpers | [`src/JB721TiersHookDeployer.sol`](./src/JB721TiersHookDeployer.sol), [`src/JB721TiersHookProjectDeployer.sol`](./src/JB721TiersHookProjectDeployer.sol), [`script/Deploy.s.sol`](./script/Deploy.s.sol) |
16
16
  | Shared libraries, interfaces, and resolver surface | [`src/libraries/`](./src/libraries/), [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/) |
17
- | Invariants, E2E flows, and regressions | [`test/invariants/`](./test/invariants/), [`test/E2E/`](./test/E2E/), [`test/regression/`](./test/regression/), [`test/TestVotingUnitsLifecycle.t.sol`](./test/TestVotingUnitsLifecycle.t.sol) |
17
+ | Mint, pricing, voting, and checkpoint behavior | [`test/TestVotingUnitsLifecycle.t.sol`](./test/TestVotingUnitsLifecycle.t.sol), [`test/TestCheckpoints.t.sol`](./test/TestCheckpoints.t.sol) |
18
+ | Reentrancy, forks, and pinned edge cases | [`test/TestSafeTransferReentrancy.t.sol`](./test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](./test/721HookAttacks.t.sol), [`test/Fork.t.sol`](./test/Fork.t.sol), [`test/TestAuditGaps.sol`](./test/TestAuditGaps.sol) |
18
19
 
19
20
  ## Repo Map
20
21
 
@@ -37,6 +38,11 @@ Tiered ERC-721 NFT issuance and cash-out hook for Juicebox V6. This repo control
37
38
  ## Working Rules
38
39
 
39
40
  - Start in [`src/JB721TiersHook.sol`](./src/JB721TiersHook.sol) for pay and cash-out behavior, but verify storage-side assumptions in [`src/JB721TiersHookStore.sol`](./src/JB721TiersHookStore.sol) before changing mint, burn, reserve, or supply logic.
41
+ - The store is the source of truth for supply, reserve, removal, and tier-order invariants. Do not “fix” those concepts only in the hook layer.
42
+ - Pending reserves are part of live economics, not deferred bookkeeping. Cash-out denominators and tier availability both depend on them before reserves are minted.
43
+ - Pay credits, overspending protection, and tier split forwarding are economically relevant. Treat them like accounting, not just UX.
40
44
  - Treat tier splits, reserve minting, and discounted pricing as treasury-sensitive. Check both runtime code and regression coverage before assuming a change is local.
45
+ - Discounted mint price and cash-out weight are intentionally not the same thing. Free or discounted mints can still carry full tier cash-out weight by design.
46
+ - Changing the default reserve beneficiary is not cosmetic. It can change which tiers have pending reserves and therefore change redemption economics for existing mints.
41
47
  - When a task mentions token metadata or rendering, confirm whether the behavior lives in this repo or in an external resolver. Do not over-edit the hook when the real change belongs downstream.
42
48
  - When changing deployers or initialization, verify the hook, store, and project-launch path stay aligned. These flows are tightly coupled.
package/USER_JOURNEYS.md CHANGED
@@ -1,96 +1,191 @@
1
1
  # User Journeys
2
2
 
3
- ## Who This Repo Serves
3
+ ## Repo Purpose
4
+
5
+ This repo is the standard tiered NFT hook for V6 projects.
6
+ It owns tier issuance, reserve accounting, hook-aware mint and cash-out behavior, and deployer packaging for hook
7
+ clones or hook-shaped project launches. It does not own collection-specific rendering or app-layer policy built on top
8
+ of the hook.
9
+
10
+ ## Primary Actors
4
11
 
5
12
  - project owners selling or rewarding supporters with tiered NFTs
6
13
  - operators managing tier supply, pricing, reserves, and ruleset-aware hook behavior
7
14
  - supporters minting or cashing out tier positions through normal Juicebox flows
8
- - integrators composing custom token URI resolvers on top of the standard 721 hook
15
+ - integrators composing custom token URI resolvers or downstream products on top of the hook
16
+
17
+ ## Key Surfaces
18
+
19
+ - `JB721TiersHook`: project-facing hook behavior for minting, reserves, metadata, and cash out
20
+ - `JB721TiersHookStore`: tier definitions and accounting backend
21
+ - `JB721TiersHookDeployer`: clone factory for existing projects
22
+ - `JB721TiersHookProjectDeployer`: project-launch packaging for new hook-backed projects
9
23
 
10
24
  ## Journey 1: Add A Tiered 721 Hook To An Existing Project
11
25
 
12
- **Starting state:** the project already exists in Juicebox and needs tiered NFT issuance without relaunching.
26
+ **Actor:** project owner or deployer.
27
+
28
+ **Intent:** attach tiered NFT behavior to an existing project without relaunching it.
29
+
30
+ **Preconditions**
31
+ - the project already exists in Juicebox
32
+ - the owner knows the tier config, reserve behavior, and resolver assumptions it wants
33
+ - the next ruleset metadata can be updated safely
34
+
35
+ **Main Flow**
36
+ 1. Use `JB721TiersHookDeployer` to clone a hook for the project.
37
+ 2. Define tier config, reserve behavior, resolver choice, and per-ruleset flags.
38
+ 3. Queue or install ruleset metadata that points at the hook.
39
+ 4. Future payments can now mint tiers under the configured constraints.
13
40
 
14
- **Success:** a hook clone is deployed, initialized, and attached to the project's ruleset metadata.
41
+ **Failure Modes**
42
+ - the hook is deployed correctly but the ruleset metadata does not actually activate it
43
+ - teams treat the resolver as cosmetic when it is part of the trusted surface
15
44
 
16
- **Flow**
17
- 1. Use `JB721TiersHookDeployer` to clone a hook for the target project.
18
- 2. Define tier config, reserve behavior, token URI resolver, and per-ruleset flags.
19
- 3. Queue or install ruleset metadata that tells the project when the hook should participate in pay and cash-out flows.
20
- 4. Future payments into the project can now mint tiers under the configured constraints.
45
+ **Postconditions**
46
+ - the project has an attached hook and future rulesets can mint tiers under the configured constraints
21
47
 
22
48
  ## Journey 2: Launch A New Project With A 721 Hook Already Wired In
23
49
 
24
- **Starting state:** the product wants its project treasury and NFT logic created in one operation.
50
+ **Actor:** product team or deployer.
25
51
 
26
- **Success:** the project launches with the hook, terminals, and initial tiers already aligned.
52
+ **Intent:** launch a project whose treasury and tiered NFT logic are aligned from the first ruleset.
27
53
 
28
- **Flow**
29
- 1. Use `JB721TiersHookProjectDeployer` with launch config, rulesets, and hook config.
30
- 2. The deployer launches the project through the core protocol and deploys the hook in the same packaged flow.
31
- 3. The initial ruleset metadata points at the newly created hook so there is no post-launch rewiring gap.
32
- 4. The project starts life as a tiered 721 project instead of becoming one later.
54
+ **Preconditions**
55
+ - the team has launch config, terminal config, and initial tier config ready
56
+
57
+ **Main Flow**
58
+ 1. Use `JB721TiersHookProjectDeployer` with launch and hook config.
59
+ 2. Launch the project and deploy the hook in the same packaged flow.
60
+ 3. Ensure the first ruleset already points at the created hook.
61
+ 4. Start life as a hook-backed project instead of converting later.
62
+
63
+ **Failure Modes**
64
+ - deployers assume the package is purely convenience and miss the initial ruleset implications
65
+ - launch-time metadata drifts from the actual hook config
66
+
67
+ **Postconditions**
68
+ - the project launches with tiered NFT logic active from the first ruleset
33
69
 
34
70
  ## Journey 3: Mint Specific Tiers Through A Payment
35
71
 
36
- **Starting state:** the project has live tiers and the payer knows which tiers they want.
72
+ **Actor:** payer.
37
73
 
38
- **Success:** the treasury receives funds and the beneficiary receives the intended NFT tiers plus any accompanying project-token behavior.
74
+ **Intent:** mint one or more tiers through a normal payment flow.
39
75
 
40
- **Flow**
41
- 1. The payer submits a payment with metadata encoding the desired tier selections.
42
- 2. `JB721TiersHook` validates tier availability, quantity rules, discounts, category constraints, and any ruleset flags affecting minting.
76
+ **Preconditions**
77
+ - the project has live tiers
78
+ - the payer submits metadata encoding the intended tier selections
79
+
80
+ **Main Flow**
81
+ 1. Submit a payment with tier-selection metadata.
82
+ 2. `JB721TiersHook` validates availability, quantity rules, discounts, and ruleset flags.
43
83
  3. `JB721TiersHookStore` updates supply and reserve accounting.
44
- 4. The hook mints the correct NFTs and the underlying terminal completes treasury accounting.
84
+ 4. The hook mints the intended NFTs and the terminal completes treasury accounting.
85
+
86
+ **Failure Modes**
87
+ - sold-out tiers, malformed metadata, or cross-currency pricing mistakes
88
+ - pay-hook participation flags do not match the user's assumptions
89
+ - split-routing or hook behavior changes what part of the payment actually mints
45
90
 
46
- **Failure cases that matter:** sold-out tiers, bad metadata, cross-currency pricing mistakes, paused pay-hook behavior, and split-routing edge cases when part of the payment should bypass normal minting assumptions.
91
+ **Postconditions**
92
+ - the intended tiers are minted and store accounting reflects the updated supply and reserve state
47
93
 
48
94
  ## Journey 4: Mint Reserves And Operate Tier Inventory Over Time
49
95
 
50
- **Starting state:** the collection is live and the owner needs to manage what exists for future minters versus reserve recipients.
96
+ **Actor:** owner or authorized operator.
97
+
98
+ **Intent:** manage tier inventory and reserve behavior after launch.
51
99
 
52
- **Success:** reserves, new tiers, removed tiers, and editable fields evolve without corrupting ownership or supply history.
100
+ **Preconditions**
101
+ - the collection is live
102
+ - the operator has the required permission surfaces to mutate tiers or mint reserves
53
103
 
54
- **Flow**
55
- 1. Authorized operators use the hook's tier-management surfaces to add, remove, or adjust tiers.
56
- 2. Reserve minting uses the configured reserve frequency and reserve beneficiary settings instead of ad hoc treasury actions.
57
- 3. The store keeps historical tier state coherent so existing token IDs continue to resolve correctly.
58
- 4. Downstream products such as Croptop, Banny, and Revnets can continue assuming stable tier semantics.
104
+ **Main Flow**
105
+ 1. Use tier-management surfaces to add, remove, or adjust tiers.
106
+ 2. Mint reserves through the configured reserve logic.
107
+ 3. Let the store preserve historical tier state so old token IDs still resolve correctly.
108
+ 4. Keep downstream products assuming stable tier semantics whenever possible.
109
+
110
+ **Failure Modes**
111
+ - tier mutations surprise downstream products or resolvers
112
+ - reserve accounting is misread as ordinary minting
113
+
114
+ **Postconditions**
115
+ - live tier inventory and reserve state match the operator's configured collection policy
59
116
 
60
117
  ## Journey 5: Let Holders Cash Out Tier Positions
61
118
 
62
- **Starting state:** a holder owns one or more NFTs from a hook-enabled project and the active ruleset allows surplus-backed exits.
119
+ **Actor:** holder.
120
+
121
+ **Intent:** exit a tier position through the project's cash-out path.
63
122
 
64
- **Success:** the holder burns the intended tier exposure and receives the correct reclaim value through the core terminal.
123
+ **Preconditions**
124
+ - the holder owns one or more NFTs from the hook-enabled project
125
+ - the active ruleset allows a surplus-backed exit
65
126
 
66
- **Flow**
67
- 1. The holder calls the project's cash-out path on the terminal.
68
- 2. The hook participates in the cash-out calculation so tier-specific weight and store state are reflected correctly.
69
- 3. The terminal settles reclaim value and the NFT position is burned or otherwise marked as consumed by the exit path.
127
+ **Main Flow**
128
+ 1. Call the project's cash-out path on the terminal.
129
+ 2. Let the hook participate in cash-out calculation so tier state is reflected.
130
+ 3. Burn or consume the tier exposure as required by the exit path.
131
+ 4. Receive the reclaim value through the terminal that holds the asset.
70
132
 
71
- **Edge conditions that change user experience:** ERC-20 cash-out surfaces, tier splits, reserve accounting drift, broken downstream terminals, and projects that use the hook for metadata only versus economically binding flows.
133
+ **Failure Modes**
134
+ - the project uses the hook for metadata only and the holder assumes an economic cash-out path exists
135
+ - terminal behavior or reserve drift changes reclaim expectations
136
+
137
+ **Postconditions**
138
+ - the holder exits the tier position through the hook-aware terminal path or learns that no such economic path is active
72
139
 
73
140
  ## Journey 6: Compose A Custom Product On Top Of The Standard Hook
74
141
 
75
- **Starting state:** the team wants product-specific rendering or business rules but does not want to fork tier issuance.
142
+ **Actor:** integrator or downstream product team.
143
+
144
+ **Intent:** build collection-specific behavior without reimplementing hook economics.
145
+
146
+ **Preconditions**
147
+ - the team wants custom presentation or app-layer logic
148
+ - the team does not want to fork pricing, reserve, and treasury behavior
76
149
 
77
- **Success:** the product resolver or wrapper composes with the hook instead of reimplementing pricing, tier accounting, and treasury logic.
150
+ **Main Flow**
151
+ 1. Plug a custom resolver or wrapper into the hook.
152
+ 2. Keep collection-specific behavior outside this repo.
153
+ 3. Audit hook-store interactions here first, then audit the downstream wrapper.
78
154
 
79
- **Flow**
80
- 1. Plug a custom resolver into the hook for metadata and product-specific presentation.
81
- 2. Keep collection-specific behavior in the downstream repo while leaving pay, reserve, and cash-out semantics in this repo.
82
- 3. Audit hook-store interactions here first, then audit the downstream resolver or wrapper.
155
+ **Failure Modes**
156
+ - downstream products reimplement hook behavior and drift from canonical accounting
157
+ - teams blame the hook for bugs that actually live in the resolver or wrapper
158
+
159
+ **Postconditions**
160
+ - the custom product reuses canonical hook economics while isolating collection-specific behavior downstream
83
161
 
84
162
  ## Journey 7: Mint NFTs To The Correct Beneficiary During Cross-Chain Payments
85
163
 
86
- **Starting state:** a sucker pays the project on behalf of a remote user via `payRemote`, and the 721 hook needs to mint NFTs and accrue credits to the real user instead of the sucker contract.
164
+ **Actor:** cross-chain payer or integrator.
165
+
166
+ **Intent:** preserve the real remote beneficiary when a sucker relays a payment.
167
+
168
+ **Preconditions**
169
+ - a sucker or relay path pays on behalf of a remote user
170
+ - relay-beneficiary metadata is encoded correctly
171
+
172
+ **Main Flow**
173
+ 1. The sucker calls `terminal.pay()` with relay-beneficiary metadata.
174
+ 2. `_mintAndUpdateCredits` resolves the relay beneficiary when `payer == beneficiary`.
175
+ 3. NFT minting and credit accounting use the resolved remote user.
176
+
177
+ **Failure Modes**
178
+ - relay metadata is missing or malformed
179
+ - downstream systems attribute NFTs or credits to the sucker instead of the user
180
+
181
+ **Postconditions**
182
+ - NFT minting and credit accounting attribute the remote payment to the correct beneficiary
87
183
 
88
- **Success:** NFTs mint to and pay credits accrue to the real remote user.
184
+ ## Trust Boundaries
89
185
 
90
- **Flow**
91
- 1. The sucker calls `terminal.pay()` with itself as both payer and beneficiary, embedding the real user's address in the `JB_RELAY_BENEFICIARY` metadata key.
92
- 2. `_mintAndUpdateCredits` detects that `payer == beneficiary` and finds relay-beneficiary metadata.
93
- 3. All NFT minting and credit accounting uses the resolved relay beneficiary instead of the sucker address.
186
+ - `JB721TiersHookStore` is the accounting backend and should be treated as part of the same economic surface as the hook
187
+ - custom token URI resolvers are part of the trusted collection surface
188
+ - core terminals remain the source of treasury accounting truth around the hook
94
189
 
95
190
  ## Hand-Offs
96
191
 
package/foundry.toml CHANGED
@@ -13,6 +13,8 @@ runs = 1024
13
13
  depth = 100
14
14
  fail_on_revert = false
15
15
 
16
+ [lint]
17
+ exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
16
18
  [fmt]
17
19
  number_underscore = "thousands"
18
20
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,9 @@
9
9
  ## Change Checklist
10
10
 
11
11
  - If you edit hook initialization, verify deployer config structs and project-launch helpers still encode the same assumptions.
12
- - If you edit tier config or metadata behavior, inspect [`src/structs/`](../src/structs/) and the corresponding interfaces in [`src/interfaces/`](../src/interfaces/).
12
+ - If you edit tier config or metadata behavior, inspect the corresponding structs and interfaces in `src/structs/` and `src/interfaces/`.
13
+ - If you edit reserve behavior, verify pending reserve counts, default reserve beneficiary semantics, and cash-out denominator effects together.
14
+ - If you edit discount behavior, verify mint price and cash-out weight separately. They are intentionally not the same quantity.
13
15
  - If you touch permissions, verify the caller path and permission constants still line up with the downstream ecosystem package that defines them.
14
16
  - If you touch URI behavior, confirm whether the issue belongs in this repo or in a downstream resolver contract that the hook calls.
15
17
 
@@ -24,5 +26,7 @@
24
26
 
25
27
  - [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
26
28
  - [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for known edge cases the repo authors considered worth pinning down.
27
- - [`test/unit/`](../test/unit/) when you need a narrow function-level proof before editing a broad runtime path.
28
- - [`script/helpers/`](../script/helpers/) when a deployment or launch question is really about config assembly rather than contract behavior.
29
+ - [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) when you need a narrow function-level proof before editing a broad runtime path.
30
+ - [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) when a local patch may have broken store-level relationships.
31
+ - [`test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol`](../test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol) when reserve-beneficiary or pending-reserve behavior changes.
32
+ - [`script/Deploy.s.sol`](../script/Deploy.s.sol) when a deployment or launch question is really about config assembly rather than contract behavior.
@@ -22,11 +22,12 @@
22
22
  - Discount behavior: price discounts affect mint eligibility but cash-out weight still tracks the original tier price. Do not conflate the two.
23
23
  - Voting units: verify whether a tier uses explicit voting units or falls back to price-based voting power before changing governance-facing math.
24
24
  - Tier removal and cleanup: removing tiers is not the same as cleaning the sorted tier list. Storage cleanup behavior matters.
25
+ - Default reserve beneficiary changes: they affect which tiers count pending reserves unless a tier-specific beneficiary overrides it. That is an economic change, not just an admin update.
25
26
 
26
27
  ## Tests To Trust First
27
28
 
28
- - [`test/invariants/`](../test/invariants/) for broad accounting invariants.
29
- - [`test/E2E/`](../test/E2E/) for launch and end-to-end payment flows.
30
- - [`test/regression/`](../test/regression/) for previously broken edge cases.
29
+ - [`test/Fork.t.sol`](../test/Fork.t.sol) for launch and live integration flows.
31
30
  - [`test/TestVotingUnitsLifecycle.t.sol`](../test/TestVotingUnitsLifecycle.t.sol) for voting-unit lifecycle behavior.
32
- - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol) and [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol) for reentrancy and attack-surface checks.
31
+ - [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) for checkpoint/module behavior.
32
+ - [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) for store-level lifecycle invariants.
33
+ - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol`](../test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol), and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for reentrancy and attack-surface checks.
@@ -110,7 +110,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
110
110
  uint256 internal _packedPricingContext;
111
111
 
112
112
  /// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
113
- /// @dev Set once during `initialize()`. Pass this to JBTokenDistributor as the IVotes token.
113
+ /// @dev Lazily deployed on the first transfer. Pass this to JBTokenDistributor as the IVotes token.
114
114
  IJB721Checkpoints public override CHECKPOINTS;
115
115
 
116
116
  //*********************************************************************//
@@ -307,9 +307,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
307
307
  || flags.preventOverspending || flags.issueTokensForSplits
308
308
  ) STORE.recordFlags(flags);
309
309
 
310
- // Deploy the checkpoint module for IVotes-compatible voting power.
311
- CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
312
-
313
310
  // Transfer ownership to the initializer.
314
311
  _transferOwnership(_msgSender());
315
312
  }
@@ -460,9 +457,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
460
457
  string calldata baseUri,
461
458
  string calldata contractUri,
462
459
  IJB721TokenUriResolver tokenUriResolver,
463
- // forge-lint: disable-next-line(mixed-case-variable)
464
460
  uint256 encodedIPFSUriTierId,
465
- // forge-lint: disable-next-line(mixed-case-variable)
466
461
  bytes32 encodedIPFSUri
467
462
  )
468
463
  external
@@ -794,6 +789,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
794
789
  // slither-disable-next-line reentrency-events,calls-loop
795
790
  STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
796
791
 
792
+ // Deploy the checkpoint module lazily on the first transfer.
793
+ if (address(CHECKPOINTS) == address(0)) {
794
+ CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
795
+ }
796
+
797
797
  // Notify the checkpoint module to update checkpointed voting power.
798
798
  CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
799
799
  }
@@ -124,7 +124,6 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
124
124
  returns (uint256 rulesetId, IJB721TiersHook hook)
125
125
  {
126
126
  // Get the project's projects contract.
127
- // forge-lint: disable-next-line(mixed-case-variable)
128
127
  IJBProjects PROJECTS = DIRECTORY.PROJECTS();
129
128
 
130
129
  // Enforce permissions.
@@ -66,7 +66,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
66
66
  /// @custom:param hook The 721 contract that the tier belongs to.
67
67
  /// @custom:param tierId The ID of the tier to get the encoded IPFS URI of.
68
68
  /// @custom:returns The encoded IPFS URI.
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
69
  mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIPFSUriOf;
71
70
 
72
71
  /// @notice Returns the largest tier ID currently used on the provided 721 contract.
@@ -1344,7 +1343,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1344
1343
  /// @notice Record a new encoded IPFS URI for a tier.
1345
1344
  /// @param tierId The ID of the tier to set the encoded IPFS URI of.
1346
1345
  /// @param encodedIPFSUri The encoded IPFS URI to set for the tier.
1347
- // forge-lint: disable-next-line(mixed-case-function, mixed-case-variable)
1346
+ // forge-lint: disable-next-line(mixed-case-function)
1348
1347
  function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external override {
1349
1348
  encodedIPFSUriOf[msg.sender][tierId] = encodedIPFSUri;
1350
1349
  }
@@ -49,7 +49,6 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
49
49
  //*********************************************************************//
50
50
 
51
51
  /// @notice The ID of the project that this contract is associated with.
52
- // forge-lint: disable-next-line(mixed-case-variable)
53
52
  uint256 public override PROJECT_ID;
54
53
 
55
54
  //*********************************************************************//
@@ -234,9 +234,7 @@ interface IJB721TiersHook is IJB721Hook {
234
234
  string calldata baseUri,
235
235
  string calldata contractUri,
236
236
  IJB721TokenUriResolver tokenUriResolver,
237
- // forge-lint: disable-next-line(mixed-case-variable)
238
237
  uint256 encodedIPFSUriTierId,
239
- // forge-lint: disable-next-line(mixed-case-variable)
240
238
  bytes32 encodedIPFSUri
241
239
  )
242
240
  external;
@@ -247,7 +247,7 @@ interface IJB721TiersHookStore {
247
247
  /// @notice Record a new encoded IPFS URI for a tier.
248
248
  /// @param tierId The ID of the tier to set the encoded IPFS URI of.
249
249
  /// @param encodedIPFSUri The encoded IPFS URI to set for the tier.
250
- // forge-lint: disable-next-line(mixed-case-function, mixed-case-variable)
250
+ // forge-lint: disable-next-line(mixed-case-function)
251
251
  function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external;
252
252
 
253
253
  /// @notice Record a newly set token URI resolver.
@@ -8,6 +8,5 @@ library JB721Constants {
8
8
  /// @notice The metadata ID used to identify the 721 beneficiary entry in payment metadata.
9
9
  /// @dev When a sucker pays on behalf of a remote user, the real user's address is embedded under this key
10
10
  /// so NFTs mint to the correct recipient.
11
- // forge-lint: disable-next-line(mixed-case-variable)
12
11
  bytes4 public constant BENEFICIARY_METADATA_ID = bytes4(keccak256("JB_721_BENEFICIARY"));
13
12
  }
@@ -8,7 +8,6 @@ import {JB721TierConfig} from "./JB721TierConfig.sol";
8
8
  /// @custom:member tiers The tiers to initialize the hook with.
9
9
  /// @custom:member currency The currency that the tier prices are denoted in. See `JBPrices`.
10
10
  /// @custom:member decimals The number of decimals in the fixed point tier prices.
11
- // forge-lint: disable-next-line(pascal-case-struct)
12
11
  struct JB721InitTiersConfig {
13
12
  JB721TierConfig[] tiers;
14
13
  uint32 currency;
@@ -21,7 +21,6 @@ import {JB721TierFlags} from "./JB721TierFlags.sol";
21
21
  /// an NFT from this tier is minted. Out of `JBConstants.SPLITS_TOTAL_PERCENT`.
22
22
  /// @custom:member resolvedUri A resolved token URI for NFTs in this tier. Only available if the NFT this tier belongs
23
23
  /// to has a resolver.
24
- // forge-lint: disable-next-line(pascal-case-struct)
25
24
  struct JB721Tier {
26
25
  uint32 id;
27
26
  uint104 price;
@@ -30,7 +29,6 @@ struct JB721Tier {
30
29
  uint104 votingUnits;
31
30
  uint16 reserveFrequency;
32
31
  address reserveBeneficiary;
33
- // forge-lint: disable-next-line(mixed-case-variable)
34
32
  bytes32 encodedIPFSUri;
35
33
  uint24 category;
36
34
  uint8 discountPercent;
@@ -23,14 +23,12 @@ import {JB721TierConfigFlags} from "./JB721TierConfigFlags.sol";
23
23
  /// an NFT from this tier is minted. Out of `JBConstants.SPLITS_TOTAL_PERCENT`.
24
24
  /// @custom:member splits The splits to use for this tier's split group. These define where the split portion of the
25
25
  /// tier's price gets routed when an NFT from this tier is minted.
26
- // forge-lint: disable-next-line(pascal-case-struct)
27
26
  struct JB721TierConfig {
28
27
  uint104 price;
29
28
  uint32 initialSupply;
30
29
  uint32 votingUnits;
31
30
  uint16 reserveFrequency;
32
31
  address reserveBeneficiary;
33
- // forge-lint: disable-next-line(mixed-case-variable)
34
32
  bytes32 encodedIPFSUri;
35
33
  uint24 category;
36
34
  uint8 discountPercent;
@@ -14,7 +14,6 @@ pragma solidity ^0.8.0;
14
14
  /// @custom:member cantIncreaseDiscountPercent If the tier cannot have its discount increased.
15
15
  /// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits. Only fresh
16
16
  /// payment value counts toward this tier's price.
17
- // forge-lint: disable-next-line(pascal-case-struct)
18
17
  struct JB721TierConfigFlags {
19
18
  bool allowOwnerMint;
20
19
  bool useReserveBeneficiaryAsDefault;
@@ -7,7 +7,6 @@ pragma solidity ^0.8.0;
7
7
  /// @custom:member cantBeRemoved A boolean indicating whether attempts to remove this tier will revert.
8
8
  /// @custom:member cantIncreaseDiscountPercent If the tier cannot have its discount increased.
9
9
  /// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits.
10
- // forge-lint: disable-next-line(pascal-case-struct)
11
10
  struct JB721TierFlags {
12
11
  bool allowOwnerMint;
13
12
  bool transfersPausable;
@@ -11,7 +11,6 @@ pragma solidity ^0.8.0;
11
11
  /// the NFTs being minted will revert.
12
12
  /// @custom:member issueTokensForSplits A boolean indicating whether payers receive token credit for the portion of
13
13
  /// their payment that is routed to tier splits. When false (default), weight is reduced proportionally.
14
- // forge-lint: disable-next-line(pascal-case-struct)
15
14
  struct JB721TiersHookFlags {
16
15
  bool noNewTiersWithReserves;
17
16
  bool noNewTiersWithVotes;
@@ -3,7 +3,6 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  /// @custom:member tierId The ID of the tier to mint from.
5
5
  /// @custom:member count The number of NFTs to mint from that tier.
6
- // forge-lint: disable-next-line(pascal-case-struct)
7
6
  struct JB721TiersMintReservesConfig {
8
7
  uint32 tierId;
9
8
  uint16 count;
@@ -6,7 +6,6 @@ pragma solidity ^0.8.0;
6
6
  /// @custom:member pauseTransfers A boolean indicating whether NFT transfers are paused during this ruleset.
7
7
  /// @custom:member pauseMintPendingReserves A boolean indicating whether pending/outstanding NFT reserves can be minted
8
8
  /// during this ruleset.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JB721TiersRulesetMetadata {
11
10
  bool pauseTransfers;
12
11
  bool pauseMintPendingReserves;
@@ -3,7 +3,6 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  /// @custom:member tierId The ID of the tier to set the discount percent for.
5
5
  /// @custom:member discountPercent The discount percent to set for the tier.
6
- // forge-lint: disable-next-line(pascal-case-struct)
7
6
  struct JB721TiersSetDiscountPercentConfig {
8
7
  uint32 tierId;
9
8
  uint16 discountPercent;
@@ -5,7 +5,6 @@ pragma solidity ^0.8.0;
5
5
  /// `JBBitmap` matrix is a "word".
6
6
  /// @custom:member The information stored at the index.
7
7
  /// @custom:member The index.
8
- // forge-lint: disable-next-line(pascal-case-struct)
9
8
  struct JBBitmapWord {
10
9
  uint256 currentWord;
11
10
  uint256 currentDepth;
@@ -12,7 +12,6 @@ import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol";
12
12
  /// @custom:member contractUri The URI where this contract's metadata can be found.
13
13
  /// @custom:member tiersConfig The NFT tiers and pricing config to launch the hook with.
14
14
  /// @custom:member flags A set of boolean options to configure the hook with.
15
- // forge-lint: disable-next-line(pascal-case-struct)
16
15
  struct JBDeploy721TiersHookConfig {
17
16
  string name;
18
17
  string symbol;
@@ -10,7 +10,6 @@ import {JBPayDataHookRulesetConfig} from "./JBPayDataHookRulesetConfig.sol";
10
10
  /// @custom:member rulesetConfigurations The ruleset configurations to queue.
11
11
  /// @custom:member terminalConfigurations The terminal configurations to add for the project.
12
12
  /// @custom:member memo A memo to pass along to the emitted event.
13
- // forge-lint: disable-next-line(pascal-case-struct)
14
13
  struct JBLaunchProjectConfig {
15
14
  string projectUri;
16
15
  JBPayDataHookRulesetConfig[] rulesetConfigurations;
@@ -9,7 +9,6 @@ import {JBPayDataHookRulesetConfig} from "./JBPayDataHookRulesetConfig.sol";
9
9
  /// @custom:member rulesetConfigurations The ruleset configurations to queue.
10
10
  /// @custom:member terminalConfigurations The terminal configurations to add for the project.
11
11
  /// @custom:member memo A memo to pass along to the emitted event.
12
- // forge-lint: disable-next-line(pascal-case-struct)
13
12
  struct JBLaunchRulesetsConfig {
14
13
  uint56 projectId;
15
14
  JBPayDataHookRulesetConfig[] rulesetConfigurations;
@@ -32,7 +32,6 @@ import {JBPayDataHookRulesetMetadata} from "./JBPayDataHookRulesetMetadata.sol";
32
32
  /// its balance in each payment terminal while the ruleset is active. Amounts are fixed point numbers using the same
33
33
  /// number of decimals as the corresponding terminal. The `payoutLimit` and `surplusAllowance` parameters must fit in
34
34
  /// a `uint232`.
35
- // forge-lint: disable-next-line(pascal-case-struct)
36
35
  struct JBPayDataHookRulesetConfig {
37
36
  uint48 mustStartAtOrAfter;
38
37
  uint32 duration;
@@ -28,7 +28,6 @@ pragma solidity ^0.8.0;
28
28
  /// during
29
29
  /// this ruleset.
30
30
  /// @custom:member metadata Metadata of the metadata, up to uint8 in size.
31
- // forge-lint: disable-next-line(pascal-case-struct)
32
31
  struct JBPayDataHookRulesetMetadata {
33
32
  uint16 reservedPercent;
34
33
  uint16 cashOutTaxRate;
@@ -6,7 +6,6 @@ import {JBPayDataHookRulesetConfig} from "./JBPayDataHookRulesetConfig.sol";
6
6
  /// @custom:member projectId The ID of the project to queue rulesets for.
7
7
  /// @custom:member rulesetConfigurations The ruleset configurations to queue.
8
8
  /// @custom:member memo A memo to pass along to the emitted event.
9
- // forge-lint: disable-next-line(pascal-case-struct)
10
9
  struct JBQueueRulesetsConfig {
11
10
  uint56 projectId;
12
11
  JBPayDataHookRulesetConfig[] rulesetConfigurations;
@@ -13,7 +13,6 @@ pragma solidity ^0.8.0;
13
13
  /// purchased.
14
14
  /// @custom:member packedBools Packed boolean flags: allowOwnerMint, transfersPausable, useVotingUnits,
15
15
  /// cantBeRemoved, cantIncreaseDiscountPercent, cantBuyWithCredits.
16
- // forge-lint: disable-next-line(pascal-case-struct)
17
16
  struct JBStored721Tier {
18
17
  uint104 price;
19
18
  uint32 remainingSupply;