@bananapus/721-hook-v6 0.0.12 → 0.0.14
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/ARCHITECTURE.md +7 -2
- package/RISKS.md +6 -6
- package/SKILLS.md +3 -2
- package/STYLE_GUIDE.md +131 -43
- package/foundry.toml +3 -3
- package/package.json +5 -5
- package/remappings.txt +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +1 -1
- package/src/JB721TiersHook.sol +23 -0
- package/src/libraries/JB721TiersHookLib.sol +44 -0
- package/test/Fork.t.sol +13 -18
- package/test/fork/ERC20TierSplitFork.t.sol +537 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -2
- package/test/unit/pay_CrossCurrency_Unit.t.sol +498 -0
- package/test/unit/tierSplitRouting_Unit.t.sol +257 -0
- /package/test/regression/{L35_CacheTierLookup.t.sol → CacheTierLookup.t.sol} +0 -0
- /package/test/regression/{L34_ReserveBeneficiaryOverwrite.t.sol → ReserveBeneficiaryOverwrite.t.sol} +0 -0
- /package/test/regression/{L36_SplitNoBeneficiary.t.sol → SplitNoBeneficiary.t.sol} +0 -0
- /package/test/unit/{M6_TierSupplyCheck.t.sol → TierSupplyReserveCheck.t.sol} +0 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -26,13 +26,18 @@ src/
|
|
|
26
26
|
### NFT Minting (via Payment)
|
|
27
27
|
```
|
|
28
28
|
User → JBMultiTerminal.pay(metadata)
|
|
29
|
+
→ beforePayRecordedWith()
|
|
30
|
+
→ calculateSplitAmounts(): per-tier split amounts (in tier pricing denomination)
|
|
31
|
+
→ convertSplitAmounts(): convert to payment token denomination (if currencies differ)
|
|
32
|
+
→ Adjust weight down by split fraction
|
|
29
33
|
→ JBTerminalStore records payment
|
|
30
|
-
→
|
|
34
|
+
→ afterPayRecordedWith()
|
|
31
35
|
→ Decode tier IDs from metadata
|
|
32
36
|
→ For each tier:
|
|
33
37
|
→ Validate: not removed, not paused, supply available
|
|
34
|
-
→ Check price (with optional discount)
|
|
38
|
+
→ Check price (with optional discount, normalized to tier pricing currency)
|
|
35
39
|
→ Mint NFT to beneficiary
|
|
40
|
+
→ Distribute split funds to tier split recipients
|
|
36
41
|
→ Leftover amount optionally mints best-available tiers
|
|
37
42
|
```
|
|
38
43
|
|
package/RISKS.md
CHANGED
|
@@ -90,10 +90,10 @@ Deep implementation-level risk analysis covering all contracts in the 721 tiered
|
|
|
90
90
|
### R-9: Price Feed Dependency -- DoS Vector
|
|
91
91
|
|
|
92
92
|
- **Severity**: MEDIUM
|
|
93
|
-
- **Location**: `JB721TiersHookLib.sol`
|
|
94
|
-
- **Description**: When the hook's pricing currency differs from the payment currency and a `JBPrices` contract is configured, the hook calls `prices.pricePerUnitOf()` to
|
|
95
|
-
- **Tested**:
|
|
96
|
-
- **Mitigation**: If `address(prices) == address(0)`, payments in non-matching currencies silently return `(0, false)` and the hook skips minting
|
|
93
|
+
- **Location**: `JB721TiersHookLib.sol` — `normalizePaymentValue` and `convertSplitAmounts`
|
|
94
|
+
- **Description**: When the hook's pricing currency differs from the payment currency and a `JBPrices` contract is configured, the hook calls `prices.pricePerUnitOf()` in two places: (1) `normalizePaymentValue` to convert the payment amount for tier price comparison, and (2) `convertSplitAmounts` to convert per-tier split amounts back to the payment token denomination for forwarding. If the price feed reverts (e.g., stale Chainlink data, sequencer down on L2), all payments in non-native currencies will revert. This is a DoS vector but not a fund-loss vector.
|
|
95
|
+
- **Tested**: YES — `test/unit/pay_CrossCurrency_Unit.t.sol` tests 8-9 verify reverting price feeds block both `normalizePaymentValue` and `convertSplitAmounts`. Live cross-currency split conversion tested in `deploy-all-v6/test/fork/CrossCurrencyFork.t.sol`.
|
|
96
|
+
- **Mitigation**: If `address(prices) == address(0)`, payments in non-matching currencies silently return `(0, false)` and the hook skips minting. `convertSplitAmounts` also returns early if no prices contract is set. Projects using cross-currency pricing should monitor feed health.
|
|
97
97
|
|
|
98
98
|
### R-10: Large Tier Array Gas Exhaustion
|
|
99
99
|
|
|
@@ -258,7 +258,7 @@ An attacker could observe a large cash-out and front-run it with their own cash-
|
|
|
258
258
|
| R-6 Soft removal cash-out weight | YES (attacks t5) | YES (INV-721-2) | YES |
|
|
259
259
|
| R-7 Pay credit payer/beneficiary | PARTIAL | YES (INV-721-3) | YES |
|
|
260
260
|
| R-8 Reserve supply protection | YES (M6) | YES (INV-721-1,4) | YES |
|
|
261
|
-
| R-9 Price feed DoS |
|
|
261
|
+
| R-9 Price feed DoS | YES (cc t8-t9) | NO | NO |
|
|
262
262
|
| R-10 Large tier array gas | PARTIAL | NO | NO |
|
|
263
263
|
| R-11 Metadata decode silent skip | PARTIAL | NO | NO |
|
|
264
264
|
| R-12 ERC-721 receiver DoS | NO | NO | NO |
|
|
@@ -285,7 +285,7 @@ An attacker could observe a large cash-out and front-run it with their own cash-
|
|
|
285
285
|
### Notable Coverage Gaps
|
|
286
286
|
|
|
287
287
|
1. **No reentrancy test** for the split distribution `.call{value}` path.
|
|
288
|
-
2. **
|
|
288
|
+
2. **Price feed failure** is tested: `test/unit/pay_CrossCurrency_Unit.t.sol` tests 8-9 verify reverting price feeds block both payment normalization and split conversion. Live feed integration tested in `deploy-all-v6`.
|
|
289
289
|
3. **No gas limit test** for operations with many tiers (hundreds+).
|
|
290
290
|
4. **No test** for token URI resolver returning malicious/reverting data.
|
|
291
291
|
5. **No test** for `initialize()` front-running on deterministic clones.
|
package/SKILLS.md
CHANGED
|
@@ -59,7 +59,8 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
|
|
|
59
59
|
| `cashOutWeightOf(hook, tokenIds)` | `JB721TiersHookStore` | Returns combined cash out weight for specific token IDs. Uses original tier price, not discounted. |
|
|
60
60
|
| `votingUnitsOf(hook, account)` | `JB721TiersHookStore` | Returns total voting units for an address across all tiers. Uses custom `votingUnits` if `useVotingUnits` is set, otherwise uses tier price. |
|
|
61
61
|
| `tierVotingUnitsOf(hook, account, tierId)` | `JB721TiersHookStore` | Returns voting units for an address within a specific tier. |
|
|
62
|
-
| `calculateSplitAmounts(store, hook, metadataIdTarget, metadata)` | `JB721TiersHookLib` | Called in `beforePayRecordedWith`. Decodes tier IDs from payer metadata, looks up each tier's `splitPercent`, calculates `mulDiv(price, splitPercent, SPLITS_TOTAL_PERCENT)` per tier, returns `totalSplitAmount`
|
|
62
|
+
| `calculateSplitAmounts(store, hook, metadataIdTarget, metadata)` | `JB721TiersHookLib` | Called in `beforePayRecordedWith`. Decodes tier IDs from payer metadata, looks up each tier's `splitPercent`, calculates `mulDiv(price, splitPercent, SPLITS_TOTAL_PERCENT)` per tier, returns `totalSplitAmount` and encoded `hookMetadata` (tier IDs + amounts). Amounts are in the tier pricing denomination — call `convertSplitAmounts` afterward when the payment currency differs. |
|
|
63
|
+
| `convertSplitAmounts(totalSplitAmount, splitMetadata, packedPricingContext, projectId, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts per-tier split amounts from tier pricing denomination to payment token denomination using `JBPrices.pricePerUnitOf`. Called automatically by `beforePayRecordedWith` when `totalSplitAmount != 0`. Returns early (no-op) when currencies match or no prices contract is configured. |
|
|
63
64
|
| `distributeAll(directory, splits, projectId, hookAddress, token, encodedSplitData)` | `JB721TiersHookLib` | Called in `afterPayRecordedWith`. Decodes per-tier amounts, looks up each tier's splits from `JBSplits` by group ID (`hookAddress | (tierId << 160)`), distributes to split recipients. Leftover goes to project balance via `addToBalance`. |
|
|
64
65
|
| `adjustTiersFor(store, splits, projectId, hookAddress, caller, tiersToAdd, tierIdsToRemove)` | `JB721TiersHookLib` | Called via DELEGATECALL from `adjustTiers`. Removes tiers, adds tiers, emits events, and registers any configured splits directly in `JBSplits`. |
|
|
65
66
|
| `normalizePaymentValue(packedPricingContext, projectId, amountValue, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts a payment value to the hook's pricing currency using `JBPrices`. Returns `(0, false)` if currencies differ and no prices contract is set. |
|
|
@@ -138,7 +139,7 @@ Each tier has configurable voting power:
|
|
|
138
139
|
- Each tier can route a percentage of its mint price to configured split recipients. The `splitPercent` field (out of `JBConstants.SPLITS_TOTAL_PERCENT` = 1,000,000,000) determines how much of the price is forwarded.
|
|
139
140
|
- Split recipients are stored in `JBSplits` using group IDs computed as `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
|
|
140
141
|
- Splits are registered in `JBSplits` both during `initialize()` (for tiers included at launch) and during `adjustTiers()` (for tiers added later), using the hook's `SPLITS` immutable directly.
|
|
141
|
-
- In `beforePayRecordedWith`, `calculateSplitAmounts` decodes tier IDs from payer metadata, computes `mulDiv(price, splitPercent, SPLITS_TOTAL_PERCENT)` per tier, and returns the total to be forwarded to the hook. The weight is adjusted down proportionally unless the `issueTokensForSplits` flag is set, in which case the full `context.weight` is returned.
|
|
142
|
+
- In `beforePayRecordedWith`, `calculateSplitAmounts` decodes tier IDs from payer metadata, computes `mulDiv(price, splitPercent, SPLITS_TOTAL_PERCENT)` per tier, and returns the total to be forwarded to the hook. If the payment currency differs from the tier pricing currency, `convertSplitAmounts` converts the amounts to the payment token denomination using the configured `JBPrices` contract. The weight is adjusted down proportionally unless the `issueTokensForSplits` flag is set, in which case the full `context.weight` is returned.
|
|
142
143
|
- In `afterPayRecordedWith`, `distributeAll` distributes forwarded funds to each tier's split group recipients. Leftover after all splits goes back to the project's balance via `addToBalance`.
|
|
143
144
|
- Split recipients can be projects (via `terminal.pay` or `terminal.addToBalance`) or plain addresses (direct ETH transfer or `SafeERC20.safeTransfer`). Splits with no `projectId` and no `beneficiary` are skipped -- their share stays in the leftover and is routed to the project's own balance via `addToBalanceOf`, preventing a misconfigured split from bricking the payout distribution.
|
|
144
145
|
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -17,8 +17,6 @@ src/
|
|
|
17
17
|
|
|
18
18
|
One contract/interface/struct/enum per file. Name the file after the type it contains.
|
|
19
19
|
|
|
20
|
-
**Structs, enums, libraries, and interfaces always go in their subdirectories** (`src/structs/`, `src/enums/`, `src/libraries/`, `src/interfaces/`) — never inline in contract files or placed in `src/` root. This keeps type definitions discoverable and import paths consistent across repos.
|
|
21
|
-
|
|
22
20
|
## Pragma Versions
|
|
23
21
|
|
|
24
22
|
```solidity
|
|
@@ -106,14 +104,6 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
106
104
|
// -------------------------- constructor ---------------------------- //
|
|
107
105
|
//*********************************************************************//
|
|
108
106
|
|
|
109
|
-
//*********************************************************************//
|
|
110
|
-
// ---------------------- receive / fallback ------------------------- //
|
|
111
|
-
//*********************************************************************//
|
|
112
|
-
|
|
113
|
-
//*********************************************************************//
|
|
114
|
-
// --------------------------- modifiers ----------------------------- //
|
|
115
|
-
//*********************************************************************//
|
|
116
|
-
|
|
117
107
|
//*********************************************************************//
|
|
118
108
|
// ---------------------- external transactions ---------------------- //
|
|
119
109
|
//*********************************************************************//
|
|
@@ -141,28 +131,23 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
141
131
|
```
|
|
142
132
|
|
|
143
133
|
**Section order:**
|
|
144
|
-
1.
|
|
145
|
-
2.
|
|
146
|
-
3.
|
|
147
|
-
4.
|
|
148
|
-
5.
|
|
149
|
-
6.
|
|
150
|
-
7.
|
|
151
|
-
8.
|
|
152
|
-
9.
|
|
153
|
-
10.
|
|
154
|
-
11.
|
|
155
|
-
12.
|
|
156
|
-
13.
|
|
157
|
-
14.
|
|
158
|
-
15. Internal helpers
|
|
159
|
-
16. Internal views
|
|
160
|
-
17. Private helpers
|
|
134
|
+
1. Custom errors
|
|
135
|
+
2. Public constants
|
|
136
|
+
3. Internal constants
|
|
137
|
+
4. Public immutable stored properties
|
|
138
|
+
5. Internal immutable stored properties
|
|
139
|
+
6. Public stored properties
|
|
140
|
+
7. Internal stored properties
|
|
141
|
+
8. Constructor
|
|
142
|
+
9. External transactions
|
|
143
|
+
10. External views
|
|
144
|
+
11. Public transactions
|
|
145
|
+
12. Internal helpers
|
|
146
|
+
13. Internal views
|
|
147
|
+
14. Private helpers
|
|
161
148
|
|
|
162
149
|
Functions are alphabetized within each section.
|
|
163
150
|
|
|
164
|
-
**Events:** Events are declared in interfaces only, never in implementation contracts. Implementations inherit events from their interface and emit them unqualified. This keeps the ABI definition in one place and allows tests to use interface-qualified event expectations (e.g., `emit IJBController.LaunchProject(...)`).
|
|
165
|
-
|
|
166
151
|
## Interface Structure
|
|
167
152
|
|
|
168
153
|
```solidity
|
|
@@ -334,9 +319,6 @@ optimizer_runs = 200
|
|
|
334
319
|
libs = ["node_modules", "lib"]
|
|
335
320
|
fs_permissions = [{ access = "read-write", path = "./"}]
|
|
336
321
|
|
|
337
|
-
[profile.ci_sizes]
|
|
338
|
-
optimizer_runs = 200
|
|
339
|
-
|
|
340
322
|
[fuzz]
|
|
341
323
|
runs = 4096
|
|
342
324
|
|
|
@@ -351,12 +333,14 @@ multiline_func_header = "all"
|
|
|
351
333
|
wrap_comments = true
|
|
352
334
|
```
|
|
353
335
|
|
|
354
|
-
**
|
|
355
|
-
- `
|
|
356
|
-
- `
|
|
357
|
-
- `optimizer = false` only for deploy-all-v6 (stack-too-deep with optimization)
|
|
336
|
+
**Optional sections (add only when needed):**
|
|
337
|
+
- `[rpc_endpoints]` — repos with fork tests. Maps named endpoints to env vars (e.g. `ethereum = "${RPC_ETHEREUM_MAINNET}"`).
|
|
338
|
+
- `[profile.ci_sizes]` — only when CI needs different optimizer settings than defaults for the size check step (e.g. `optimizer_runs = 200` when the default profile uses a lower value).
|
|
358
339
|
|
|
359
|
-
|
|
340
|
+
**Common variations:**
|
|
341
|
+
- `via_ir = true` when hitting stack-too-deep
|
|
342
|
+
- `optimizer = false` when optimization causes stack-too-deep
|
|
343
|
+
- `optimizer_runs` reduced when deep struct nesting causes stack-too-deep at 200 runs
|
|
360
344
|
|
|
361
345
|
### CI Workflows
|
|
362
346
|
|
|
@@ -386,8 +370,10 @@ jobs:
|
|
|
386
370
|
uses: foundry-rs/foundry-toolchain@v1
|
|
387
371
|
- name: Run tests
|
|
388
372
|
run: forge test --fail-fast --summary --detailed --skip "*/script/**"
|
|
373
|
+
env:
|
|
374
|
+
RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
|
|
389
375
|
- name: Check contract sizes
|
|
390
|
-
run:
|
|
376
|
+
run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
|
|
391
377
|
```
|
|
392
378
|
|
|
393
379
|
**lint.yml:**
|
|
@@ -409,11 +395,60 @@ jobs:
|
|
|
409
395
|
run: forge fmt --check
|
|
410
396
|
```
|
|
411
397
|
|
|
398
|
+
**slither.yml** (repos with `src/` contracts only):
|
|
399
|
+
```yaml
|
|
400
|
+
name: slither
|
|
401
|
+
on:
|
|
402
|
+
pull_request:
|
|
403
|
+
branches:
|
|
404
|
+
- main
|
|
405
|
+
push:
|
|
406
|
+
branches:
|
|
407
|
+
- main
|
|
408
|
+
jobs:
|
|
409
|
+
analyze:
|
|
410
|
+
runs-on: ubuntu-latest
|
|
411
|
+
steps:
|
|
412
|
+
- uses: actions/checkout@v4
|
|
413
|
+
with:
|
|
414
|
+
submodules: recursive
|
|
415
|
+
- uses: actions/setup-node@v4
|
|
416
|
+
with:
|
|
417
|
+
node-version: latest
|
|
418
|
+
- name: Install npm dependencies
|
|
419
|
+
run: npm install --omit=dev
|
|
420
|
+
- name: Install Foundry
|
|
421
|
+
uses: foundry-rs/foundry-toolchain@v1
|
|
422
|
+
- name: Run slither
|
|
423
|
+
uses: crytic/slither-action@v0.3.1
|
|
424
|
+
with:
|
|
425
|
+
slither-config: slither-ci.config.json
|
|
426
|
+
fail-on: medium
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**slither-ci.config.json:**
|
|
430
|
+
```json
|
|
431
|
+
{
|
|
432
|
+
"detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
|
|
433
|
+
"exclude_informational": true,
|
|
434
|
+
"exclude_low": false,
|
|
435
|
+
"exclude_medium": false,
|
|
436
|
+
"exclude_high": false,
|
|
437
|
+
"disable_color": false,
|
|
438
|
+
"filter_paths": "(mocks/|test/|node_modules/|lib/)",
|
|
439
|
+
"legacy_ast": false
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Variations:**
|
|
444
|
+
- Deployer-only repos (no `src/`, only `script/`) skip slither entirely — the action's internal `forge build` skips `test/` and `script/` by default, leaving nothing to compile.
|
|
445
|
+
- Use inline `// slither-disable-next-line <detector>` to suppress known false positives rather than adding to `detectors_to_exclude` in the config. The comment must be on the line immediately before the flagged expression.
|
|
446
|
+
|
|
412
447
|
### package.json
|
|
413
448
|
|
|
414
449
|
```json
|
|
415
450
|
{
|
|
416
|
-
"name": "@bananapus/
|
|
451
|
+
"name": "@bananapus/package-name-v6",
|
|
417
452
|
"version": "x.x.x",
|
|
418
453
|
"license": "MIT",
|
|
419
454
|
"repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
|
|
@@ -433,13 +468,62 @@ jobs:
|
|
|
433
468
|
|
|
434
469
|
### remappings.txt
|
|
435
470
|
|
|
436
|
-
Every repo has a `remappings.txt
|
|
471
|
+
Every repo has a `remappings.txt` as the **single source of truth** for import remappings. Never add remappings to `foundry.toml`.
|
|
472
|
+
|
|
473
|
+
**Principle:** Import paths in Solidity source must match npm package names exactly. With `libs = ["node_modules", "lib"]`, Foundry auto-resolves `@scope/package/path/File.sol` → `node_modules/@scope/package/path/File.sol`. No remapping needed for packages installed as real directories.
|
|
474
|
+
|
|
475
|
+
**Note:** Auto-resolution does **not** work for symlinked packages (e.g. npm workspace links). Workspace repos like `deploy-all-v6` and `nana-cli-v6` need explicit `@scope/package/=node_modules/@scope/package/` remappings for each symlinked dependency.
|
|
476
|
+
|
|
477
|
+
**Minimal content** (most repos):
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
forge-std/=lib/forge-std/src/
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Only add extra remappings for:
|
|
484
|
+
- **`forge-std`** — always needed (git submodule with `src/` subdirectory)
|
|
485
|
+
- **Repo-specific `lib/` submodules** that have no npm package (e.g., `hookmate/=lib/hookmate/src/`)
|
|
486
|
+
- **Symlinked npm packages** — need explicit `@scope/package/=node_modules/@scope/package/` entries
|
|
487
|
+
- **Nested transitive deps** — e.g., `@chainlink/contracts-ccip/` nested inside `@bananapus/suckers-v6/node_modules/`
|
|
488
|
+
|
|
489
|
+
**Never add remappings for:**
|
|
490
|
+
- npm packages that match their import path and are installed as real directories — they auto-resolve
|
|
491
|
+
- Short-form aliases (e.g., `@bananapus/core/` → `@bananapus/core-v6/src/`) — fix the import instead
|
|
492
|
+
- Packages available via npm that are also git submodules — remove the submodule, use npm
|
|
437
493
|
|
|
494
|
+
**Import path convention:**
|
|
495
|
+
|
|
496
|
+
| Package | Import path | Resolves to |
|
|
497
|
+
|---------|------------|-------------|
|
|
498
|
+
| `@bananapus/core-v6` | `@bananapus/core-v6/src/libraries/JBConstants.sol` | `node_modules/@bananapus/core-v6/src/...` |
|
|
499
|
+
| `@openzeppelin/contracts` | `@openzeppelin/contracts/token/ERC20/IERC20.sol` | `node_modules/@openzeppelin/contracts/...` |
|
|
500
|
+
| `@uniswap/v4-core` | `@uniswap/v4-core/src/interfaces/IPoolManager.sol` | `node_modules/@uniswap/v4-core/src/...` |
|
|
501
|
+
|
|
502
|
+
### Linting
|
|
503
|
+
|
|
504
|
+
Solar (Foundry's built-in linter) runs automatically during `forge build`. It scans all `.sol` files in `libs` directories, including `node_modules`.
|
|
505
|
+
|
|
506
|
+
**All test helpers must use relative imports** (e.g. `../../src/structs/JBRuleset.sol`), not bare `src/` imports. This ensures solar can resolve paths when the helper is consumed via npm in downstream repos.
|
|
507
|
+
|
|
508
|
+
### Fork Tests
|
|
509
|
+
|
|
510
|
+
Fork tests use named RPC endpoints defined in `[rpc_endpoints]` of `foundry.toml`. No skip guards — fork tests should hard-fail if the RPC endpoint is unavailable, making CI failures explicit.
|
|
511
|
+
|
|
512
|
+
```solidity
|
|
513
|
+
function setUp() public {
|
|
514
|
+
vm.createSelectFork("ethereum");
|
|
515
|
+
// ... setup code
|
|
516
|
+
}
|
|
438
517
|
```
|
|
439
|
-
|
|
518
|
+
|
|
519
|
+
The endpoint name (e.g. `"ethereum"`) maps to an env var via `foundry.toml`:
|
|
520
|
+
|
|
521
|
+
```toml
|
|
522
|
+
[rpc_endpoints]
|
|
523
|
+
ethereum = "${RPC_ETHEREUM_MAINNET}"
|
|
440
524
|
```
|
|
441
525
|
|
|
442
|
-
|
|
526
|
+
For multi-chain fork tests, add all needed endpoints.
|
|
443
527
|
|
|
444
528
|
### Formatting
|
|
445
529
|
|
|
@@ -467,4 +551,8 @@ CI checks formatting via `forge fmt --check`.
|
|
|
467
551
|
|
|
468
552
|
### Contract Size Checks
|
|
469
553
|
|
|
470
|
-
CI runs `
|
|
554
|
+
CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
|
|
555
|
+
|
|
556
|
+
## Repo-Specific Deviations
|
|
557
|
+
|
|
558
|
+
None. This repo follows the standard configuration exactly.
|
package/foundry.toml
CHANGED
|
@@ -5,9 +5,6 @@ optimizer_runs = 200
|
|
|
5
5
|
libs = ["node_modules", "lib"]
|
|
6
6
|
fs_permissions = [{ access = "read-write", path = "./"}]
|
|
7
7
|
|
|
8
|
-
[profile.ci_sizes]
|
|
9
|
-
optimizer_runs = 200
|
|
10
|
-
|
|
11
8
|
[fuzz]
|
|
12
9
|
runs = 4096
|
|
13
10
|
|
|
@@ -20,3 +17,6 @@ fail_on_revert = false
|
|
|
20
17
|
number_underscore = "thousands"
|
|
21
18
|
multiline_func_header = "all"
|
|
22
19
|
wrap_comments = true
|
|
20
|
+
|
|
21
|
+
[rpc_endpoints]
|
|
22
|
+
ethereum = "${RPC_ETHEREUM_MAINNET}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bananapus/address-registry-v6": "^0.0.
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
20
|
+
"@bananapus/address-registry-v6": "^0.0.8",
|
|
21
|
+
"@bananapus/core-v6": "^0.0.15",
|
|
22
22
|
"@bananapus/ownable-v6": "^0.0.7",
|
|
23
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
24
|
-
"@openzeppelin/contracts": "5.
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.7",
|
|
24
|
+
"@openzeppelin/contracts": "^5.6.1",
|
|
25
25
|
"@prb/math": "^4.1.0",
|
|
26
26
|
"solady": "^0.1.8"
|
|
27
27
|
},
|
package/remappings.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
forge-std/=lib/forge-std/src/
|
package/script/Deploy.s.sol
CHANGED
|
@@ -4,7 +4,7 @@ pragma solidity 0.8.26;
|
|
|
4
4
|
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
5
5
|
import "@bananapus/address-registry-v6/script/helpers/AddressRegistryDeploymentLib.sol";
|
|
6
6
|
|
|
7
|
-
import {Sphinx} from "@sphinx-labs/contracts/SphinxPlugin.sol";
|
|
7
|
+
import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
|
|
8
8
|
import {Script} from "forge-std/Script.sol";
|
|
9
9
|
|
|
10
10
|
import {JB721TiersHookDeployer} from "../src/JB721TiersHookDeployer.sol";
|
|
@@ -4,7 +4,7 @@ pragma solidity 0.8.26;
|
|
|
4
4
|
import {stdJson} from "forge-std/Script.sol";
|
|
5
5
|
import {Vm} from "forge-std/Vm.sol";
|
|
6
6
|
|
|
7
|
-
import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/SphinxConstants.sol";
|
|
7
|
+
import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/contracts/foundry/SphinxConstants.sol";
|
|
8
8
|
|
|
9
9
|
import {IJB721TiersHookDeployer} from "../../src/interfaces/IJB721TiersHookDeployer.sol";
|
|
10
10
|
import {IJB721TiersHookProjectDeployer} from "../../src/interfaces/IJB721TiersHookProjectDeployer.sol";
|
package/src/JB721TiersHook.sol
CHANGED
|
@@ -7,6 +7,7 @@ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
|
7
7
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
8
8
|
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
9
9
|
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
10
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
10
11
|
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
11
12
|
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
12
13
|
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
@@ -16,6 +17,8 @@ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
|
16
17
|
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
17
18
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
18
19
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
20
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
21
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
19
22
|
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
20
23
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
21
24
|
import {JB721Hook} from "./abstract/JB721Hook.sol";
|
|
@@ -38,6 +41,8 @@ import {JB721TiersSetDiscountPercentConfig} from "./structs/JB721TiersSetDiscoun
|
|
|
38
41
|
/// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
|
|
39
42
|
/// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
|
|
40
43
|
contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook {
|
|
44
|
+
using SafeERC20 for IERC20;
|
|
45
|
+
|
|
41
46
|
//*********************************************************************//
|
|
42
47
|
// --------------------------- custom errors ------------------------- //
|
|
43
48
|
//*********************************************************************//
|
|
@@ -186,6 +191,18 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
186
191
|
(uint256 totalSplitAmount, bytes memory splitMetadata) =
|
|
187
192
|
JB721TiersHookLib.calculateSplitAmounts(STORE, address(this), METADATA_ID_TARGET, context.metadata);
|
|
188
193
|
|
|
194
|
+
// Convert split amounts from tier pricing to payment token denomination if currencies differ.
|
|
195
|
+
if (totalSplitAmount != 0) {
|
|
196
|
+
(totalSplitAmount, splitMetadata) = JB721TiersHookLib.convertSplitAmounts(
|
|
197
|
+
totalSplitAmount,
|
|
198
|
+
splitMetadata,
|
|
199
|
+
_packedPricingContext,
|
|
200
|
+
context.projectId,
|
|
201
|
+
context.amount.currency,
|
|
202
|
+
context.amount.decimals
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
// Adjust weight so the terminal mints tokens only for the amount that actually enters the project.
|
|
190
207
|
if (totalSplitAmount == 0 || STORE.flagsOf(address(this)).issueTokensForSplits) {
|
|
191
208
|
// No splits, or hook configured to give full token credit regardless — full weight.
|
|
@@ -675,6 +692,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
675
692
|
|
|
676
693
|
// Distribute any forwarded funds to tier split groups.
|
|
677
694
|
if (context.hookMetadata.length != 0 && context.forwardedAmount.value != 0) {
|
|
695
|
+
// For ERC20 tokens, pull from terminal using the allowance it granted via _beforeTransferTo.
|
|
696
|
+
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
697
|
+
IERC20(context.forwardedAmount.token)
|
|
698
|
+
.safeTransferFrom(msg.sender, address(this), context.forwardedAmount.value);
|
|
699
|
+
}
|
|
700
|
+
|
|
678
701
|
JB721TiersHookLib.distributeAll(
|
|
679
702
|
DIRECTORY, SPLITS, PROJECT_ID, address(this), context.forwardedAmount.token, context.hookMetadata
|
|
680
703
|
);
|
|
@@ -189,6 +189,50 @@ library JB721TiersHookLib {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/// @notice Converts split amounts from tier pricing denomination to payment token denomination.
|
|
193
|
+
/// @dev Called after `calculateSplitAmounts` when the payment currency differs from the tier pricing currency.
|
|
194
|
+
/// @param totalSplitAmount The total split amount in tier pricing denomination.
|
|
195
|
+
/// @param splitMetadata The encoded per-tier breakdown (tierIds, amounts) from calculateSplitAmounts.
|
|
196
|
+
/// @param packedPricingContext The packed pricing context (currency, decimals, prices address).
|
|
197
|
+
/// @param projectId The project ID.
|
|
198
|
+
/// @param amountCurrency The payment amount currency.
|
|
199
|
+
/// @param amountDecimals The payment amount decimals.
|
|
200
|
+
/// @return convertedTotal The total split amount converted to payment token denomination.
|
|
201
|
+
/// @return convertedMetadata The re-encoded per-tier breakdown with converted amounts.
|
|
202
|
+
function convertSplitAmounts(
|
|
203
|
+
uint256 totalSplitAmount,
|
|
204
|
+
bytes memory splitMetadata,
|
|
205
|
+
uint256 packedPricingContext,
|
|
206
|
+
uint256 projectId,
|
|
207
|
+
uint256 amountCurrency,
|
|
208
|
+
uint256 amountDecimals
|
|
209
|
+
)
|
|
210
|
+
external
|
|
211
|
+
view
|
|
212
|
+
returns (uint256 convertedTotal, bytes memory convertedMetadata)
|
|
213
|
+
{
|
|
214
|
+
uint256 pricingCurrency = uint256(uint32(packedPricingContext));
|
|
215
|
+
if (amountCurrency == pricingCurrency) return (totalSplitAmount, splitMetadata);
|
|
216
|
+
|
|
217
|
+
IJBPrices prices = IJBPrices(address(uint160(packedPricingContext >> 40)));
|
|
218
|
+
if (address(prices) == address(0)) return (totalSplitAmount, splitMetadata);
|
|
219
|
+
|
|
220
|
+
uint256 pricingDecimals = uint256(uint8(packedPricingContext >> 32));
|
|
221
|
+
uint256 ratio = prices.pricePerUnitOf({
|
|
222
|
+
projectId: projectId,
|
|
223
|
+
pricingCurrency: amountCurrency,
|
|
224
|
+
unitCurrency: pricingCurrency,
|
|
225
|
+
decimals: amountDecimals
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
(uint16[] memory tierIds, uint256[] memory amounts) = abi.decode(splitMetadata, (uint16[], uint256[]));
|
|
229
|
+
for (uint256 i; i < amounts.length; i++) {
|
|
230
|
+
amounts[i] = mulDiv(amounts[i], ratio, 10 ** pricingDecimals);
|
|
231
|
+
convertedTotal += amounts[i];
|
|
232
|
+
}
|
|
233
|
+
convertedMetadata = abi.encode(tierIds, amounts);
|
|
234
|
+
}
|
|
235
|
+
|
|
192
236
|
/// @notice Sets split groups in JBSplits for tiers that have splits configured.
|
|
193
237
|
function _setSplitGroupsFor(
|
|
194
238
|
IJBSplits splits,
|
package/test/Fork.t.sol
CHANGED
|
@@ -122,12 +122,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
122
122
|
receive() external payable {}
|
|
123
123
|
|
|
124
124
|
function setUp() public {
|
|
125
|
-
|
|
126
|
-
if (bytes(rpcUrl).length == 0) {
|
|
127
|
-
vm.skip(true);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
vm.createSelectFork(rpcUrl);
|
|
125
|
+
vm.createSelectFork("ethereum");
|
|
131
126
|
|
|
132
127
|
_deployJBCore();
|
|
133
128
|
_deploy721Hook();
|
|
@@ -345,7 +340,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
345
340
|
|
|
346
341
|
/// @dev Build pay metadata that requests specific tier IDs. `allowOverspending` controls revert behavior.
|
|
347
342
|
function _buildPayMetadata(
|
|
348
|
-
address
|
|
343
|
+
address,
|
|
349
344
|
uint16[] memory tierIds,
|
|
350
345
|
bool allowOverspending
|
|
351
346
|
)
|
|
@@ -361,7 +356,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
361
356
|
}
|
|
362
357
|
|
|
363
358
|
/// @dev Build cash out metadata that specifies token IDs to burn.
|
|
364
|
-
function _buildCashOutMetadata(address
|
|
359
|
+
function _buildCashOutMetadata(address, uint256[] memory tokenIds) internal view returns (bytes memory) {
|
|
365
360
|
bytes[] memory data = new bytes[](1);
|
|
366
361
|
data[0] = abi.encode(tokenIds);
|
|
367
362
|
bytes4[] memory ids = new bytes4[](1);
|
|
@@ -734,7 +729,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
734
729
|
tierConfigs[0].reserveFrequency = 0; // Initial tier has no reserves (allowed).
|
|
735
730
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
736
731
|
flags.noNewTiersWithReserves = true;
|
|
737
|
-
(
|
|
732
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
738
733
|
|
|
739
734
|
// Try to add a new tier with reserves.
|
|
740
735
|
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
@@ -767,7 +762,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
767
762
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, false);
|
|
768
763
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
769
764
|
flags.noNewTiersWithOwnerMinting = true;
|
|
770
|
-
(
|
|
765
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
771
766
|
|
|
772
767
|
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
773
768
|
newTiers[0] = JB721TierConfig({
|
|
@@ -799,7 +794,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
799
794
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, false);
|
|
800
795
|
tierConfigs[0].cannotBeRemoved = true;
|
|
801
796
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
802
|
-
(
|
|
797
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
803
798
|
|
|
804
799
|
uint256[] memory toRemove = new uint256[](1);
|
|
805
800
|
toRemove[0] = 1;
|
|
@@ -864,7 +859,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
864
859
|
tierConfigs[0].discountPercent = 50;
|
|
865
860
|
tierConfigs[0].cannotIncreaseDiscountPercent = true;
|
|
866
861
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
867
|
-
(
|
|
862
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
868
863
|
|
|
869
864
|
vm.prank(multisig);
|
|
870
865
|
vm.expectRevert();
|
|
@@ -1037,7 +1032,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1037
1032
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, true); // allowOwnerMint=true
|
|
1038
1033
|
tierConfigs[0].reserveFrequency = 0; // No reserves (required: can't have both).
|
|
1039
1034
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
1040
|
-
(
|
|
1035
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1041
1036
|
|
|
1042
1037
|
uint16[] memory tierIds = new uint16[](2);
|
|
1043
1038
|
tierIds[0] = 1;
|
|
@@ -1054,7 +1049,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1054
1049
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, true);
|
|
1055
1050
|
tierConfigs[0].reserveFrequency = 0;
|
|
1056
1051
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
1057
|
-
(
|
|
1052
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1058
1053
|
|
|
1059
1054
|
uint16[] memory tierIds = new uint16[](1);
|
|
1060
1055
|
tierIds[0] = 1;
|
|
@@ -1408,7 +1403,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1408
1403
|
|
|
1409
1404
|
/// @notice Calling initialize() again on a deployed hook should revert.
|
|
1410
1405
|
function test_fork_reInitialize_reverts() public {
|
|
1411
|
-
(
|
|
1406
|
+
(, address hook,) = _launchStandardProject();
|
|
1412
1407
|
|
|
1413
1408
|
JB721TierConfig[] memory emptyTiers = new JB721TierConfig[](0);
|
|
1414
1409
|
|
|
@@ -1468,7 +1463,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1468
1463
|
|
|
1469
1464
|
/// @notice Non-owner cannot adjustTiers.
|
|
1470
1465
|
function test_fork_adjustTiers_noPermission_reverts() public {
|
|
1471
|
-
(
|
|
1466
|
+
(, address hook,) = _launchStandardProject();
|
|
1472
1467
|
|
|
1473
1468
|
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
1474
1469
|
newTiers[0] = JB721TierConfig({
|
|
@@ -1906,7 +1901,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1906
1901
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
1907
1902
|
|
|
1908
1903
|
(uint256 projectId1, address hook1) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1909
|
-
(
|
|
1904
|
+
(, address hook2) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1910
1905
|
|
|
1911
1906
|
assertTrue(hook1 != hook2, "hooks are different clones");
|
|
1912
1907
|
|
|
@@ -1989,7 +1984,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1989
1984
|
uint24 category
|
|
1990
1985
|
)
|
|
1991
1986
|
internal
|
|
1992
|
-
|
|
1987
|
+
pure
|
|
1993
1988
|
returns (JB721TierConfig memory)
|
|
1994
1989
|
{
|
|
1995
1990
|
return JB721TierConfig({
|