@bannynet/core-v6 0.0.17 → 0.0.19
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 +28 -0
- package/ARCHITECTURE.md +51 -75
- package/AUDIT_INSTRUCTIONS.md +64 -331
- package/CHANGELOG.md +31 -0
- package/README.md +53 -167
- package/RISKS.md +18 -1
- package/SKILLS.md +27 -243
- package/STYLE_GUIDE.md +56 -17
- package/USER_JOURNEYS.md +51 -496
- package/package.json +8 -8
- package/references/operations.md +25 -0
- package/references/runtime.md +27 -0
- package/script/Add.Denver.s.sol +10 -7
- package/script/Deploy.s.sol +37 -28
- package/script/Drop1.s.sol +424 -329
- package/src/Banny721TokenUriResolver.sol +109 -59
- package/test/Banny721TokenUriResolver.t.sol +8 -5
- package/test/BannyAttacks.t.sol +8 -5
- package/test/DecorateFlow.t.sol +8 -5
- package/test/Fork.t.sol +25 -17
- package/test/OutfitTransferLifecycle.t.sol +8 -5
- package/test/TestAuditGaps.sol +8 -5
- package/test/TestQALastMile.t.sol +8 -5
- package/test/audit/AntiStrandingRetention.t.sol +33 -5
- package/test/audit/BurnedBodyStrandsAssets.t.sol +9 -5
- package/test/audit/MergedOutfitExclusivity.t.sol +8 -5
- package/test/audit/MigrationHelperVerificationBypass.t.sol +102 -0
- package/test/audit/TryTransferFromStrandsAssets.t.sol +8 -5
- package/test/regression/BodyCategoryValidation.t.sol +8 -5
- package/test/regression/BurnedTokenCheck.t.sol +8 -5
- package/test/regression/CEIReorder.t.sol +8 -5
- package/test/regression/RemovedTierDesync.t.sol +8 -5
- package/CHANGE_LOG.md +0 -243
- package/assets/findings/banny-retail-v6-pashov-ai-audit-report-20260330-102839.md +0 -34
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
+
|
|
8
|
+
import {MigrationHelper} from "../../script/helpers/MigrationHelper.sol";
|
|
9
|
+
|
|
10
|
+
contract MigrationHelperVerificationBypassTest is Test {
|
|
11
|
+
address internal constant ALICE = address(0xA11CE);
|
|
12
|
+
address internal constant FALLBACK_RESOLVER = address(0xFA11BAC);
|
|
13
|
+
|
|
14
|
+
MockStore internal v4Store;
|
|
15
|
+
MockStore internal v5Store;
|
|
16
|
+
MockHook internal v4Hook;
|
|
17
|
+
MockHook internal v5Hook;
|
|
18
|
+
MigrationHelperHarness internal harness;
|
|
19
|
+
|
|
20
|
+
function setUp() public {
|
|
21
|
+
v4Store = new MockStore();
|
|
22
|
+
v5Store = new MockStore();
|
|
23
|
+
v4Hook = new MockHook(address(v4Store));
|
|
24
|
+
v5Hook = new MockHook(address(v5Store));
|
|
25
|
+
harness = new MigrationHelperHarness();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function test_verifyTierBalances_skipsAllOwnersForTierWhenFallbackResolverOwnsAnyOfTier() public {
|
|
29
|
+
address[] memory owners = new address[](1);
|
|
30
|
+
owners[0] = ALICE;
|
|
31
|
+
|
|
32
|
+
uint256[] memory tierIds = new uint256[](1);
|
|
33
|
+
tierIds[0] = 7;
|
|
34
|
+
|
|
35
|
+
// Alice is over-allocated in V5 versus V4 for tier 7.
|
|
36
|
+
v4Store.setTierBalance(address(v4Hook), ALICE, 7, 1);
|
|
37
|
+
v5Store.setTierBalance(address(v5Hook), ALICE, 7, 2);
|
|
38
|
+
|
|
39
|
+
// One unrelated V4 token of the same tier sits in the fallback resolver.
|
|
40
|
+
v4Store.setTierBalance(address(v4Hook), FALLBACK_RESOLVER, 7, 1);
|
|
41
|
+
|
|
42
|
+
// Intended behavior would reject Alice's inflation, but the helper skips the tier entirely.
|
|
43
|
+
harness.verifyTierBalances(address(v5Hook), address(v4Hook), FALLBACK_RESOLVER, owners, tierIds);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function test_verifyTierBalances_revertsWhenFallbackResolverDoesNotOwnTier() public {
|
|
47
|
+
address[] memory owners = new address[](1);
|
|
48
|
+
owners[0] = ALICE;
|
|
49
|
+
|
|
50
|
+
uint256[] memory tierIds = new uint256[](1);
|
|
51
|
+
tierIds[0] = 7;
|
|
52
|
+
|
|
53
|
+
v4Store.setTierBalance(address(v4Hook), ALICE, 7, 1);
|
|
54
|
+
v5Store.setTierBalance(address(v5Hook), ALICE, 7, 2);
|
|
55
|
+
|
|
56
|
+
vm.expectRevert(
|
|
57
|
+
bytes(
|
|
58
|
+
"V5 tier balance exceeds V4: owner=0x00000000000000000000000000000000000a11ce tier=7 v4Balance=1 v5Balance=2"
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
harness.verifyTierBalances(address(v5Hook), address(v4Hook), FALLBACK_RESOLVER, owners, tierIds);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
contract MigrationHelperHarness {
|
|
66
|
+
function verifyTierBalances(
|
|
67
|
+
address hookAddress,
|
|
68
|
+
address v4HookAddress,
|
|
69
|
+
address v4FallbackResolverAddress,
|
|
70
|
+
address[] memory owners,
|
|
71
|
+
uint256[] memory tierIds
|
|
72
|
+
)
|
|
73
|
+
external
|
|
74
|
+
view
|
|
75
|
+
{
|
|
76
|
+
MigrationHelper.verifyTierBalances(hookAddress, v4HookAddress, v4FallbackResolverAddress, owners, tierIds);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
contract MockHook {
|
|
81
|
+
address internal immutable _store;
|
|
82
|
+
|
|
83
|
+
constructor(address store) {
|
|
84
|
+
_store = store;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function STORE() external view returns (IJB721TiersHookStore) {
|
|
88
|
+
return IJB721TiersHookStore(_store);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
contract MockStore {
|
|
93
|
+
mapping(address hook => mapping(address owner => mapping(uint256 tierId => uint256))) internal _tierBalanceOf;
|
|
94
|
+
|
|
95
|
+
function setTierBalance(address hook, address owner, uint256 tierId, uint256 balance) external {
|
|
96
|
+
_tierBalanceOf[hook][owner][tierId] = balance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function tierBalanceOf(address hook, address owner, uint256 tierId) external view returns (uint256) {
|
|
100
|
+
return _tierBalanceOf[hook][owner][tierId];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
+
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
6
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
7
8
|
|
|
8
9
|
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
@@ -180,11 +181,13 @@ contract TryTransferFromStrandsAssetsTest is Test {
|
|
|
180
181
|
encodedIPFSUri: bytes32(0),
|
|
181
182
|
category: category,
|
|
182
183
|
discountPercent: 0,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
flags: JB721TierFlags({
|
|
185
|
+
allowOwnerMint: false,
|
|
186
|
+
transfersPausable: false,
|
|
187
|
+
cantBeRemoved: false,
|
|
188
|
+
cantIncreaseDiscountPercent: false,
|
|
189
|
+
cantBuyWithCredits: false
|
|
190
|
+
}),
|
|
188
191
|
splitPercent: 0,
|
|
189
192
|
resolvedUri: ""
|
|
190
193
|
});
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
+
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
6
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
7
8
|
|
|
8
9
|
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
@@ -131,11 +132,13 @@ contract BodyCategoryValidationTest is Test {
|
|
|
131
132
|
encodedIPFSUri: bytes32(0),
|
|
132
133
|
category: category,
|
|
133
134
|
discountPercent: 0,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
flags: JB721TierFlags({
|
|
136
|
+
allowOwnerMint: false,
|
|
137
|
+
transfersPausable: false,
|
|
138
|
+
cantBeRemoved: false,
|
|
139
|
+
cantIncreaseDiscountPercent: false,
|
|
140
|
+
cantBuyWithCredits: false
|
|
141
|
+
}),
|
|
139
142
|
splitPercent: 0,
|
|
140
143
|
resolvedUri: ""
|
|
141
144
|
});
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
+
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
6
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
7
8
|
|
|
8
9
|
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
@@ -170,11 +171,13 @@ contract BurnedTokenCheckTest is Test {
|
|
|
170
171
|
encodedIPFSUri: bytes32(0),
|
|
171
172
|
category: category,
|
|
172
173
|
discountPercent: 0,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
flags: JB721TierFlags({
|
|
175
|
+
allowOwnerMint: false,
|
|
176
|
+
transfersPausable: false,
|
|
177
|
+
cantBeRemoved: false,
|
|
178
|
+
cantIncreaseDiscountPercent: false,
|
|
179
|
+
cantBuyWithCredits: false
|
|
180
|
+
}),
|
|
178
181
|
splitPercent: 0,
|
|
179
182
|
resolvedUri: ""
|
|
180
183
|
});
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
+
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
6
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
7
8
|
|
|
8
9
|
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
@@ -193,11 +194,13 @@ contract CEIReorderTest is Test {
|
|
|
193
194
|
encodedIPFSUri: bytes32(0),
|
|
194
195
|
category: category,
|
|
195
196
|
discountPercent: 0,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
flags: JB721TierFlags({
|
|
198
|
+
allowOwnerMint: false,
|
|
199
|
+
transfersPausable: false,
|
|
200
|
+
cantBeRemoved: false,
|
|
201
|
+
cantIncreaseDiscountPercent: false,
|
|
202
|
+
cantBuyWithCredits: false
|
|
203
|
+
}),
|
|
201
204
|
splitPercent: 0,
|
|
202
205
|
resolvedUri: ""
|
|
203
206
|
});
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
+
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
6
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
7
8
|
|
|
8
9
|
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
@@ -330,11 +331,13 @@ contract RemovedTierDesyncTest is Test {
|
|
|
330
331
|
encodedIPFSUri: bytes32(0),
|
|
331
332
|
category: category,
|
|
332
333
|
discountPercent: 0,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
flags: JB721TierFlags({
|
|
335
|
+
allowOwnerMint: false,
|
|
336
|
+
transfersPausable: false,
|
|
337
|
+
cantBeRemoved: false,
|
|
338
|
+
cantIncreaseDiscountPercent: false,
|
|
339
|
+
cantBuyWithCredits: false
|
|
340
|
+
}),
|
|
338
341
|
splitPercent: 0,
|
|
339
342
|
resolvedUri: ""
|
|
340
343
|
});
|
package/CHANGE_LOG.md
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
# banny-retail-v6 Changelog (v5 → v6)
|
|
2
|
-
|
|
3
|
-
This document describes all changes between `banny-retail` (v5) and `banny-retail-v6` (v6).
|
|
4
|
-
|
|
5
|
-
## Summary
|
|
6
|
-
|
|
7
|
-
- **Key bug fixes**: Default eyes incorrectly selected based on outfit UPC instead of body UPC; decoration operations blocked when a previously-equipped item was burned/removed.
|
|
8
|
-
- **Fault-tolerant transfers**: New `_tryTransferFrom()` wraps returns of previously-equipped items in try-catch — a burned or removed outfit no longer blocks the entire `decorateBannyWith()` operation.
|
|
9
|
-
- **Richer metadata**: `setSvgBaseUri()` replaced by `setMetadata()` which sets description, external URL, and base URI together. Token JSON `description` and `external_url` are now dynamic.
|
|
10
|
-
- **Body category validation**: `decorateBannyWith()` now verifies the `bannyBodyId` actually belongs to a body-category tier before proceeding.
|
|
11
|
-
- **Batch setter safety**: Array length mismatch checks added to `setProductNames()`, `setSvgContentsOf()`, and `setSvgHashesOf()`.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 1. Breaking Changes
|
|
16
|
-
|
|
17
|
-
### Solidity Version Bump
|
|
18
|
-
- **v5:** `pragma solidity 0.8.23;`
|
|
19
|
-
- **v6:** `pragma solidity 0.8.28;`
|
|
20
|
-
|
|
21
|
-
### Dependency Imports Updated
|
|
22
|
-
All `@bananapus/721-hook-v5` imports replaced with `@bananapus/721-hook-v6`:
|
|
23
|
-
- `IERC721`, `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`
|
|
24
|
-
|
|
25
|
-
### `setSvgBaseUri()` Removed and Replaced by `setMetadata()`
|
|
26
|
-
- **v5:** `setSvgBaseUri(string calldata baseUri)` -- sets only the SVG base URI. Emits `SetSvgBaseUri`.
|
|
27
|
-
- **v6:** `setMetadata(string calldata description, string calldata url, string calldata baseUri)` -- sets description, external URL, and base URI in a single call. Emits `SetMetadata`.
|
|
28
|
-
- Callers that previously used `setSvgBaseUri()` must migrate to `setMetadata()`.
|
|
29
|
-
|
|
30
|
-
### `setSvgHashsOf()` Renamed to `setSvgHashesOf()`
|
|
31
|
-
- **v5:** `setSvgHashsOf(uint256[] memory upcs, bytes32[] memory svgHashs)`
|
|
32
|
-
- **v6:** `setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes)`
|
|
33
|
-
- Function name and parameter name corrected for proper English pluralization.
|
|
34
|
-
|
|
35
|
-
### `pricingContext()` Return Value Change
|
|
36
|
-
- **v5:** `(uint256 currency, uint256 decimals,) = IJB721TiersHook(hook).pricingContext();` -- three return values (third ignored).
|
|
37
|
-
- **v6:** `(uint256 currency, uint256 decimals) = IJB721TiersHook(hook).pricingContext();` -- two return values.
|
|
38
|
-
- Reflects an upstream change in `IJB721TiersHook` where `pricingContext()` now returns only two values.
|
|
39
|
-
|
|
40
|
-
### Token Metadata `description` and `external_url` Are Now Dynamic
|
|
41
|
-
- **v5:** Hardcoded in `tokenUriOf()`: `"description":"A piece of Banny Retail."` and `"external_url":"https://retail.banny.eth.sucks"`.
|
|
42
|
-
- **v6:** Read from state variables `svgDescription` and `svgExternalUrl`, set via `setMetadata()`. These default to empty strings until the owner sets them.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 2. New Features
|
|
47
|
-
|
|
48
|
-
### New State Variables: `svgDescription` and `svgExternalUrl`
|
|
49
|
-
- `string public svgDescription` -- the description used in token metadata JSON.
|
|
50
|
-
- `string public svgExternalUrl` -- the external URL used in token metadata JSON.
|
|
51
|
-
- Both are settable via the new `setMetadata()` function.
|
|
52
|
-
|
|
53
|
-
### Body Category Validation in `decorateBannyWith()`
|
|
54
|
-
- **v6 adds:** A check that the `bannyBodyId` actually belongs to a body-category tier (`_BODY_CATEGORY == 0`). If not, reverts with `Banny721TokenUriResolver_BannyBodyNotBodyCategory()`.
|
|
55
|
-
- **v5:** No such check existed; any token ID could be passed as a banny body.
|
|
56
|
-
|
|
57
|
-
### `_tryTransferFrom()` -- Fault-Tolerant, Return-Aware Transfers
|
|
58
|
-
- **v6 adds:** `_tryTransferFrom(address hook, address from, address to, uint256 assetId) returns (bool success)` -- wraps `safeTransferFrom` in a try-catch and returns whether the transfer succeeded.
|
|
59
|
-
- Used in `_decorateBannyWithBackground()` and `_decorateBannyWithOutfits()` when returning previously equipped items.
|
|
60
|
-
- When the return transfer fails, **state is preserved** instead of cleared — preventing NFT stranding:
|
|
61
|
-
- **Backgrounds**: Failed return aborts the entire background change (old background stays attached, new one is not equipped).
|
|
62
|
-
- **Outfits**: Failed-to-return outfits are retained in the attached list via `_storeOutfitsWithRetained()`.
|
|
63
|
-
- **v5:** Used `_transferFrom()` (which reverts on failure) for all transfers, meaning a single burned/removed outfit could block the entire decoration operation.
|
|
64
|
-
|
|
65
|
-
> **Why this mattered**: In v5, if a project owner removed a tier that contained an equipped outfit, the Banny body owner could never change decorations again — the `safeTransferFrom` for the removed item would revert, permanently blocking the `decorateBannyWith()` function. This was the most-reported user issue.
|
|
66
|
-
|
|
67
|
-
### `_storeOutfitsWithRetained()` -- Anti-Stranding Merge
|
|
68
|
-
- **v6 adds:** `_storeOutfitsWithRetained(address hook, uint256 bannyBodyId, uint256[] memory outfitIds, uint256[] memory previousOutfitIds)` -- stores the new outfit array, appending any previously equipped outfits whose return transfer failed (non-zero entries in `previousOutfitIds`).
|
|
69
|
-
- This ensures that NFTs held by the resolver but not successfully returned to the owner remain tracked and recoverable in subsequent `decorateBannyWith` calls.
|
|
70
|
-
|
|
71
|
-
### `_isInArray()` Helper
|
|
72
|
-
- **v6 adds:** `_isInArray(uint256 value, uint256[] memory array)` -- checks if a value is present in an array.
|
|
73
|
-
- Used during outfit cleanup to skip outfits being re-equipped rather than transferring them out and back in.
|
|
74
|
-
|
|
75
|
-
### Array Length Validation on Batch Setters
|
|
76
|
-
- **v6 adds:** `Banny721TokenUriResolver_ArrayLengthMismatch()` error.
|
|
77
|
-
- `setProductNames()`, `setSvgContentsOf()`, and `setSvgHashesOf()` now validate that the `upcs` and values arrays have matching lengths. v5 had no such check, risking out-of-bounds reverts.
|
|
78
|
-
|
|
79
|
-
### `assetIdsOf()` Array Resize via Assembly
|
|
80
|
-
- **v6 adds:** After filtering outfits, the returned `outfitIds` array is resized via inline assembly (`mstore(outfitIds, numberOfIncludedOutfits)`) to remove trailing zeros.
|
|
81
|
-
- **v5:** Returned the full-length array with trailing zero entries for unincluded outfits.
|
|
82
|
-
|
|
83
|
-
### `_encodeTokenUri()` Extracted Helper
|
|
84
|
-
- **v6 adds:** `_encodeTokenUri(uint256 tokenId, JB721Tier memory product, string memory extraMetadata, string memory imageContents)` -- an internal view function that encodes the token URI JSON with base64.
|
|
85
|
-
- Uses nested `abi.encodePacked()` calls to avoid "stack too deep" errors.
|
|
86
|
-
- **v5:** Inlined the entire JSON encoding in `tokenUriOf()` as a single large `abi.encodePacked()` call.
|
|
87
|
-
|
|
88
|
-
### Default Eyes Bug Fix (`_outfitContentsFor`)
|
|
89
|
-
- **v5 (bug):** `_outfitContentsFor()` used the current outfit's `upc` to decide alien vs. standard default eyes: `if (upc == ALIEN_UPC)`. This checked the UPC of the *outfit being iterated*, not the banny body.
|
|
90
|
-
- **v6 (fix):** `_outfitContentsFor()` now accepts an additional `bodyUpc` parameter and uses `if (bodyUpc == ALIEN_UPC)` to correctly select default eyes based on the banny body type.
|
|
91
|
-
|
|
92
|
-
> **Why this mattered**: The bug caused alien Bannys to get standard eyes and vice versa when any outfit was equipped, breaking the visual identity of the NFT. The fix ensures default eyes are always selected based on the body type, not whatever outfit happens to be iterated.
|
|
93
|
-
|
|
94
|
-
### Improved Background Authorization Logic
|
|
95
|
-
- **v6:** `_decorateBannyWithBackground()` now explicitly checks if an unused background (where `userId == 0`) can only be attached by its owner. In v5, the authorization check `_msgSender() != owner && _msgSender() != IERC721(hook).ownerOf(userOf(hook, backgroundId))` could behave unexpectedly when `userOf()` returned 0 (querying `ownerOf(0)` on the hook).
|
|
96
|
-
|
|
97
|
-
### CEI Pattern in `_decorateBannyWithBackground()`
|
|
98
|
-
- **v6:** Updates all state (`_attachedBackgroundIdOf`, `_userOf`) before any external transfers. Previous background transfer-out happens after state updates.
|
|
99
|
-
- **v5:** Transferred the previous background out *before* updating state for the new background, creating a less safe interaction ordering.
|
|
100
|
-
|
|
101
|
-
### Background Category Validation
|
|
102
|
-
- **v5:** Only checked `backgroundProduct.id == 0` to reject invalid backgrounds.
|
|
103
|
-
- **v6:** Also checks `backgroundProduct.category != _BACKGROUND_CATEGORY`, ensuring only actual background-category items can be used as backgrounds.
|
|
104
|
-
|
|
105
|
-
### Outfit Re-equip Optimization
|
|
106
|
-
- **v6:** When cleaning up remaining previous outfits, checks `_isInArray(previousOutfitId, outfitIds)` to skip outfits being re-equipped, avoiding unnecessary transfer-out-and-back-in cycles.
|
|
107
|
-
- **v5:** Would transfer the outfit out and then transfer it back in during the same transaction.
|
|
108
|
-
|
|
109
|
-
### Improved Loop Guard in `_decorateBannyWithOutfits()`
|
|
110
|
-
- **v5:** `while (previousOutfitProductCategory <= outfitProductCategory && previousOutfitProductCategory != 0)` -- stops on category 0 but could re-enter after exhaustion.
|
|
111
|
-
- **v6:** `while (previousOutfitId != 0 && previousOutfitProductCategory <= outfitProductCategory)` -- guards on `previousOutfitId != 0` as primary condition, correctly handling removed tiers (category 0) by always processing and advancing past them.
|
|
112
|
-
|
|
113
|
-
### Re-check Ownership Before Transfer in `_decorateBannyWithOutfits()`
|
|
114
|
-
- **v5:** Cached `owner = IERC721(hook).ownerOf(outfitId)` at the top of the loop, then later checked `if (owner != address(this))`.
|
|
115
|
-
- **v6:** Re-checks `IERC721(hook).ownerOf(outfitId) != address(this)` at transfer time, avoiding stale ownership data after intermediate transfers.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## 3. Event Changes
|
|
120
|
-
|
|
121
|
-
### Added
|
|
122
|
-
| Event | Signature |
|
|
123
|
-
|-------|-----------|
|
|
124
|
-
| `SetMetadata` | `SetMetadata(string description, string externalUrl, string baseUri, address caller)` |
|
|
125
|
-
|
|
126
|
-
### Removed
|
|
127
|
-
| Event | Signature |
|
|
128
|
-
|-------|-----------|
|
|
129
|
-
| `SetSvgBaseUri` | `SetSvgBaseUri(string baseUri, address caller)` |
|
|
130
|
-
|
|
131
|
-
### Unchanged
|
|
132
|
-
| Event | Notes |
|
|
133
|
-
|-------|-------|
|
|
134
|
-
| `DecorateBanny` | Same signature in both versions |
|
|
135
|
-
| `SetProductName` | Same signature in both versions |
|
|
136
|
-
| `SetSvgContent` | Same signature in both versions |
|
|
137
|
-
| `SetSvgHash` | Same signature in both versions |
|
|
138
|
-
|
|
139
|
-
### `msg.sender` Replaced with `_msgSender()` in Event Emissions
|
|
140
|
-
- **v5:** `setProductNames()`, `setSvgBaseUri()`, `setSvgContentsOf()`, and `setSvgHashsOf()` used `msg.sender` in event emissions.
|
|
141
|
-
- **v6:** All event emissions consistently use `_msgSender()` (ERC-2771 compatible).
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
## 4. Error Changes
|
|
146
|
-
|
|
147
|
-
### Added
|
|
148
|
-
| Error | Purpose |
|
|
149
|
-
|-------|---------|
|
|
150
|
-
| `Banny721TokenUriResolver_ArrayLengthMismatch()` | Reverts when batch setter arrays have mismatched lengths |
|
|
151
|
-
| `Banny721TokenUriResolver_BannyBodyNotBodyCategory()` | Reverts when `decorateBannyWith()` is called with a non-body-category token |
|
|
152
|
-
|
|
153
|
-
### Unchanged
|
|
154
|
-
| Error |
|
|
155
|
-
|-------|
|
|
156
|
-
| `Banny721TokenUriResolver_CantAccelerateTheLock()` |
|
|
157
|
-
| `Banny721TokenUriResolver_ContentsAlreadyStored()` |
|
|
158
|
-
| `Banny721TokenUriResolver_ContentsMismatch()` |
|
|
159
|
-
| `Banny721TokenUriResolver_HashAlreadyStored()` |
|
|
160
|
-
| `Banny721TokenUriResolver_HashNotFound()` |
|
|
161
|
-
| `Banny721TokenUriResolver_HeadAlreadyAdded()` |
|
|
162
|
-
| `Banny721TokenUriResolver_OutfitChangesLocked()` |
|
|
163
|
-
| `Banny721TokenUriResolver_SuitAlreadyAdded()` |
|
|
164
|
-
| `Banny721TokenUriResolver_UnauthorizedBackground()` |
|
|
165
|
-
| `Banny721TokenUriResolver_UnauthorizedBannyBody()` |
|
|
166
|
-
| `Banny721TokenUriResolver_UnauthorizedOutfit()` |
|
|
167
|
-
| `Banny721TokenUriResolver_UnauthorizedTransfer()` |
|
|
168
|
-
| `Banny721TokenUriResolver_UnorderedCategories()` |
|
|
169
|
-
| `Banny721TokenUriResolver_UnrecognizedBackground()` |
|
|
170
|
-
| `Banny721TokenUriResolver_UnrecognizedCategory()` |
|
|
171
|
-
| `Banny721TokenUriResolver_UnrecognizedProduct()` |
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## 5. Struct Changes
|
|
176
|
-
|
|
177
|
-
No struct changes. Both versions use `JB721Tier` from the respective `721-hook` dependency. Any changes to `JB721Tier` are upstream in `nana-721-hook-v6`.
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## 6. Implementation Changes (Non-Interface)
|
|
182
|
-
|
|
183
|
-
### Token URI JSON Encoding Refactored
|
|
184
|
-
- **v5:** Single large `abi.encodePacked()` call with all JSON fields inlined in `tokenUriOf()`.
|
|
185
|
-
- **v6:** Split into `pricingMetadata` string built separately, then delegated to `_encodeTokenUri()`. Uses nested `abi.encodePacked()` to avoid "stack too deep".
|
|
186
|
-
|
|
187
|
-
### `_outfitContentsFor()` Signature Change
|
|
188
|
-
- **v5:** `_outfitContentsFor(address hook, uint256[] memory outfitIds)`
|
|
189
|
-
- **v6:** `_outfitContentsFor(address hook, uint256[] memory outfitIds, uint256 bodyUpc)` -- added `bodyUpc` parameter for correct default eyes selection.
|
|
190
|
-
|
|
191
|
-
### `_bannyBodySvgOf()` Relocated
|
|
192
|
-
- **v5:** Located after `_msgSender()` / `_msgData()` overrides (line ~700).
|
|
193
|
-
- **v6:** Relocated to immediately before `_categoryNameOf()` (line ~538), grouped with other internal view functions.
|
|
194
|
-
|
|
195
|
-
### `_contextSuffixLength()` Relocated
|
|
196
|
-
- **v5:** Located before `_bannyBodySvgOf()` (line ~562).
|
|
197
|
-
- **v6:** Relocated after `_categoryNameOf()` and `_bannyBodySvgOf()` (line ~616).
|
|
198
|
-
|
|
199
|
-
### Named Parameters in `JBIpfsDecoder.decode()` Calls
|
|
200
|
-
- **v5:** Positional arguments: `JBIpfsDecoder.decode(baseUri, ...)`.
|
|
201
|
-
- **v6:** Named arguments: `JBIpfsDecoder.decode({baseUri: baseUri, hexString: ...})`.
|
|
202
|
-
|
|
203
|
-
### NatDoc / Comment Improvements
|
|
204
|
-
- Typo fixes: "Nakes" to "Naked", "receieved" to "received", "prefered" to "preferred", "categorie's" to "category's", "transfered" to "transferred", "scg" to "svg", "lateset" to "latest".
|
|
205
|
-
- Added detailed NatDoc to all interface functions (v5 interface had no NatDoc).
|
|
206
|
-
- Added documentation for outfit travel behavior on banny body transfer.
|
|
207
|
-
- Added documentation for unbounded array gas considerations on `_attachedOutfitIdsOf`.
|
|
208
|
-
- Added detailed authorization rules in `decorateBannyWith()` NatDoc (6-point checklist).
|
|
209
|
-
- Added warning about outfit/background travel on banny body transfer.
|
|
210
|
-
- Added comment about `transferFrom` vs `safeTransferFrom` limitation in `onERC721Received`.
|
|
211
|
-
|
|
212
|
-
### Lint Suppression Comments
|
|
213
|
-
- **v6 adds:** `// forge-lint: disable-next-line(mixed-case-variable)` above `DEFAULT_ALIEN_EYES`, `DEFAULT_MOUTH`, `DEFAULT_NECKLACE`, `DEFAULT_STANDARD_EYES`, and `BANNY_BODY`.
|
|
214
|
-
|
|
215
|
-
### Import Order Change
|
|
216
|
-
- **v5:** `@bananapus` imports first, then OpenZeppelin imports.
|
|
217
|
-
- **v6:** OpenZeppelin imports first, then `@bananapus` imports.
|
|
218
|
-
|
|
219
|
-
### Slither Annotations
|
|
220
|
-
- **v6 adds:** `// slither-disable-next-line calls-loop` on several `IERC721(hook).ownerOf()` calls inside loops.
|
|
221
|
-
- **v6 adds:** `// slither-disable-next-line encode-packed-collision` on the `_encodeTokenUri()` return.
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## 7. Migration Table
|
|
226
|
-
|
|
227
|
-
| v5 Function / Event | v6 Equivalent | Notes |
|
|
228
|
-
|---|---|---|
|
|
229
|
-
| `setSvgBaseUri(string)` | `setMetadata(string, string, string)` | Now sets description + external URL + base URI together |
|
|
230
|
-
| `setSvgHashsOf(uint256[], bytes32[])` | `setSvgHashesOf(uint256[], bytes32[])` | Renamed (typo fix) |
|
|
231
|
-
| `SetSvgBaseUri` event | `SetMetadata` event | Different parameters |
|
|
232
|
-
| N/A | `svgDescription` (state variable) | New |
|
|
233
|
-
| N/A | `svgExternalUrl` (state variable) | New |
|
|
234
|
-
| N/A | `Banny721TokenUriResolver_ArrayLengthMismatch` | New error |
|
|
235
|
-
| N/A | `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | New error |
|
|
236
|
-
| `_transferFrom()` (for returning items) | `_tryTransferFrom()` | Fault-tolerant; `_transferFrom()` still exists for mandatory transfers |
|
|
237
|
-
| N/A | `_isInArray()` | New helper |
|
|
238
|
-
| N/A | `_encodeTokenUri()` | Extracted from `tokenUriOf()` |
|
|
239
|
-
| `_outfitContentsFor(hook, outfitIds)` | `_outfitContentsFor(hook, outfitIds, bodyUpc)` | Added `bodyUpc` param (bug fix) |
|
|
240
|
-
| `@bananapus/721-hook-v5` | `@bananapus/721-hook-v6` | Dependency upgrade |
|
|
241
|
-
| `pragma solidity 0.8.23` | `pragma solidity 0.8.28` | Compiler version bump |
|
|
242
|
-
|
|
243
|
-
> **Cross-repo impact**: The `pricingContext()` return change (3 values → 2) is driven by the upstream `nana-721-hook-v6` `IJB721TiersHook` interface change. The `@bananapus/721-hook-v6` dependency brings in the new tier splits system, though Banny Retail does not use tier splits.
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# 🔐 Security Review — banny-retail-v6
|
|
2
|
-
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Scope
|
|
6
|
-
|
|
7
|
-
| | |
|
|
8
|
-
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
-
| **Mode** | ALL / default |
|
|
10
|
-
| **Files reviewed** | `Add.Denver.s.sol` · `Deploy.s.sol` · `Drop1.s.sol`<br>`BannyverseDeploymentLib.sol` · `MigrationHelper.sol` · `Banny721TokenUriResolver.sol` |
|
|
11
|
-
| **Confidence threshold (1-100)** | 75 |
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Findings
|
|
16
|
-
|
|
17
|
-
_No confirmed findings._
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
Findings List
|
|
22
|
-
|
|
23
|
-
| # | Confidence | Title |
|
|
24
|
-
|---|---|---|
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Leads
|
|
29
|
-
|
|
30
|
-
_None._
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
> ⚠️ This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. For a consultation regarding your projects' security, visit [https://www.pashov.com](https://www.pashov.com)
|