@bannynet/core-v6 0.0.10 → 0.0.11
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 +14 -12
- package/README.md +19 -6
- package/RISKS.md +1 -1
- package/USER_JOURNEYS.md +1 -1
- package/package.json +7 -7
- package/src/Banny721TokenUriResolver.sol +16 -0
- package/test/DecorateFlow.t.sol +31 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -17,25 +17,27 @@ src/
|
|
|
17
17
|
|
|
18
18
|
### Asset Storage
|
|
19
19
|
```
|
|
20
|
-
Owner →
|
|
21
|
-
→
|
|
22
|
-
→ Content stored in multiple chunks for large SVGs
|
|
20
|
+
Owner → setSvgHashesOf(upcs, hashes)
|
|
21
|
+
→ Register content hashes for UPCs (owner-only)
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
→
|
|
26
|
-
→
|
|
23
|
+
Anyone → setSvgContentsOf(upcs, contents)
|
|
24
|
+
→ Upload SVG content matching registered hashes
|
|
25
|
+
→ Content validated against hash before storage
|
|
26
|
+
|
|
27
|
+
Owner → setProductNames(upcs, names)
|
|
28
|
+
→ Register product names for UPCs
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
### Outfit Composition
|
|
30
32
|
```
|
|
31
|
-
|
|
32
|
-
→ Attach outfit
|
|
33
|
-
→ Outfit NFTs transferred to resolver contract
|
|
33
|
+
Body Owner → decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
|
|
34
|
+
→ Attach outfit and background NFTs to a body NFT
|
|
35
|
+
→ Outfit/background NFTs transferred to resolver contract
|
|
36
|
+
→ Previous outfits returned to owner
|
|
34
37
|
→ Composite SVG generated from layered components
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
→
|
|
38
|
-
→ Outfit NFTs returned to holder
|
|
39
|
+
Body Owner → lockOutfitChangesFor(hook, bodyId)
|
|
40
|
+
→ Lock outfit changes for 7 days
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
### Token URI Generation
|
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ Banny is a composable NFT character system built on top of Juicebox 721 hooks. E
|
|
|
23
23
|
→ Body's tokenURI now renders the full dressed composition
|
|
24
24
|
|
|
|
25
25
|
4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
|
|
26
|
-
→ Freezes outfit changes for 7 days
|
|
26
|
+
→ Freezes outfit and background changes for 7 days
|
|
27
|
+
→ Prevents moving currently equipped assets away through another body's decoration call
|
|
27
28
|
→ Proves the Banny's look is stable (useful for PFPs, displays)
|
|
28
29
|
|
|
|
29
30
|
5. SVG content is stored on-chain via a two-step process:
|
|
@@ -92,20 +93,32 @@ Requires `via_ir = true` in foundry.toml due to stack depth in SVG composition.
|
|
|
92
93
|
| Command | Description |
|
|
93
94
|
|---------|-------------|
|
|
94
95
|
| `forge build` | Compile contracts (requires via-IR) |
|
|
95
|
-
| `forge test` | Run all tests (
|
|
96
|
+
| `forge test` | Run all tests (14 test files: functionality, attacks, decoration flows, fork integration, transfer lifecycle, audit gaps, QA, regressions) |
|
|
96
97
|
| `forge test -vvv` | Run tests with full trace |
|
|
97
98
|
|
|
98
99
|
## Repository Layout
|
|
99
100
|
|
|
100
101
|
```
|
|
101
102
|
src/
|
|
102
|
-
Banny721TokenUriResolver.sol # Sole contract (~1,
|
|
103
|
+
Banny721TokenUriResolver.sol # Sole contract (~1,428 lines)
|
|
103
104
|
interfaces/
|
|
104
105
|
IBanny721TokenUriResolver.sol # Public interface + events
|
|
105
106
|
test/
|
|
106
|
-
Banny721TokenUriResolver.t.sol # Unit tests
|
|
107
|
-
BannyAttacks.t.sol # Security/adversarial tests
|
|
108
|
-
DecorateFlow.t.sol # Decoration flow tests
|
|
107
|
+
Banny721TokenUriResolver.t.sol # Unit tests
|
|
108
|
+
BannyAttacks.t.sol # Security/adversarial tests
|
|
109
|
+
DecorateFlow.t.sol # Decoration flow tests
|
|
110
|
+
Fork.t.sol # Fork integration tests
|
|
111
|
+
OutfitTransferLifecycle.t.sol # Transfer lifecycle tests
|
|
112
|
+
TestAuditGaps.sol # Audit gap coverage (meta-tx, SVG edge cases)
|
|
113
|
+
TestQALastMile.t.sol # QA tests (gas, round-trip, fallback)
|
|
114
|
+
regression/
|
|
115
|
+
ArrayLengthValidation.t.sol # Input validation regression
|
|
116
|
+
BodyCategoryValidation.t.sol # Body category regression
|
|
117
|
+
BurnedTokenCheck.t.sol # Burned token regression
|
|
118
|
+
CEIReorder.t.sol # CEI ordering regression
|
|
119
|
+
ClearMetadata.t.sol # Metadata clearing regression
|
|
120
|
+
MsgSenderEvents.t.sol # Event emission regression
|
|
121
|
+
RemovedTierDesync.t.sol # Removed tier desync regression
|
|
109
122
|
script/
|
|
110
123
|
Deploy.s.sol # Sphinx multi-chain deployment
|
|
111
124
|
Drop1.s.sol # Outfit drop deployment
|
package/RISKS.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
- **Outfit theft via banny body transfer.** Equipped outfits and backgrounds travel with the banny body NFT on transfer. If a banny body is sold with valuable outfits equipped, the buyer gains control of all equipped items. Sellers must unequip before selling. Marketplaces may not surface this risk.
|
|
13
13
|
- **try-catch silent failures.** `_tryTransferFrom` silently catches all transfer failures. If an outfit NFT is burned or its tier removed, the transfer fails silently. The outfit remains logically "equipped" in state but the NFT is lost. This can create phantom outfits that show in SVG rendering but cannot be recovered.
|
|
14
|
-
- **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before selling prevents the buyer from changing outfits for up to 7 days.
|
|
14
|
+
- **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before selling prevents the buyer from changing outfits for up to 7 days. The lock now also freezes reassignment of currently equipped outfits/backgrounds away from that body during the lock window.
|
|
15
15
|
|
|
16
16
|
## 3. Access Control
|
|
17
17
|
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -311,7 +311,7 @@ Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both
|
|
|
311
311
|
|
|
312
312
|
### Edge Cases
|
|
313
313
|
|
|
314
|
-
- **Old body is locked**:
|
|
314
|
+
- **Old body is locked**: Reverts `OutfitChangesLocked`. A locked source body keeps its currently equipped outfits and background until the lock expires, even if the caller owns both bodies.
|
|
315
315
|
|
|
316
316
|
---
|
|
317
317
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
24
|
-
"@bananapus/core-v6": "^0.0.
|
|
23
|
+
"@bananapus/721-hook-v6": "^0.0.19",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.24",
|
|
25
25
|
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
26
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
27
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
28
|
-
"@croptop/core-v6": "^0.0.
|
|
26
|
+
"@bananapus/router-terminal-v6": "^0.0.17",
|
|
27
|
+
"@bananapus/suckers-v6": "^0.0.13",
|
|
28
|
+
"@croptop/core-v6": "^0.0.20",
|
|
29
29
|
"@openzeppelin/contracts": "^5.6.1",
|
|
30
|
-
"@rev-net/core-v6": "^0.0.
|
|
30
|
+
"@rev-net/core-v6": "^0.0.15",
|
|
31
31
|
"keccak": "^3.0.4"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -1156,6 +1156,16 @@ contract Banny721TokenUriResolver is
|
|
|
1156
1156
|
// ---------------------- internal transactions ---------------------- //
|
|
1157
1157
|
//*********************************************************************//
|
|
1158
1158
|
|
|
1159
|
+
/// @notice Revert if an equipped asset is being reassigned away from a locked source body.
|
|
1160
|
+
/// @param hook The hook storing the assets.
|
|
1161
|
+
/// @param bannyBodyId The body currently using the asset.
|
|
1162
|
+
/// @param exemptBodyId The destination body currently being decorated.
|
|
1163
|
+
function _revertIfBodyLocked(address hook, uint256 bannyBodyId, uint256 exemptBodyId) internal view {
|
|
1164
|
+
if (bannyBodyId != 0 && bannyBodyId != exemptBodyId && outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
|
|
1165
|
+
revert Banny721TokenUriResolver_OutfitChangesLocked();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1159
1169
|
/// @notice Add a background to a banny body.
|
|
1160
1170
|
/// @param hook The hook storing the assets.
|
|
1161
1171
|
/// @param bannyBodyId The ID of the banny body being dressed.
|
|
@@ -1186,6 +1196,9 @@ contract Banny721TokenUriResolver is
|
|
|
1186
1196
|
if (_msgSender() != IERC721(hook).ownerOf(userId)) {
|
|
1187
1197
|
revert Banny721TokenUriResolver_UnauthorizedBackground();
|
|
1188
1198
|
}
|
|
1199
|
+
|
|
1200
|
+
// A locked source body keeps its equipped background until the lock expires.
|
|
1201
|
+
_revertIfBodyLocked({hook: hook, bannyBodyId: userId, exemptBodyId: bannyBodyId});
|
|
1189
1202
|
}
|
|
1190
1203
|
|
|
1191
1204
|
// Get the background's product info.
|
|
@@ -1274,6 +1287,9 @@ contract Banny721TokenUriResolver is
|
|
|
1274
1287
|
if (_msgSender() != IERC721(hook).ownerOf(wearerId)) {
|
|
1275
1288
|
revert Banny721TokenUriResolver_UnauthorizedOutfit();
|
|
1276
1289
|
}
|
|
1290
|
+
|
|
1291
|
+
// A locked source body keeps its equipped outfits until the lock expires.
|
|
1292
|
+
_revertIfBodyLocked({hook: hook, bannyBodyId: wearerId, exemptBodyId: bannyBodyId});
|
|
1277
1293
|
}
|
|
1278
1294
|
|
|
1279
1295
|
// Get the outfit's product info.
|
package/test/DecorateFlow.t.sol
CHANGED
|
@@ -797,6 +797,37 @@ contract DecorateFlowTests is Test {
|
|
|
797
797
|
assertEq(hook.ownerOf(NECKLACE_1), bob, "necklace returned to bob");
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
+
/// @notice A locked body keeps its equipped background until the lock expires, even if the owner also controls an
|
|
801
|
+
/// unlocked destination body.
|
|
802
|
+
function test_lock_preventsMovingBackgroundFromLockedBody() public {
|
|
803
|
+
vm.prank(alice);
|
|
804
|
+
resolver.decorateBannyWith(address(hook), BODY_A, BACKGROUND_1, new uint256[](0));
|
|
805
|
+
|
|
806
|
+
vm.prank(alice);
|
|
807
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_A);
|
|
808
|
+
|
|
809
|
+
vm.prank(alice);
|
|
810
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
|
|
811
|
+
resolver.decorateBannyWith(address(hook), BODY_B, BACKGROUND_1, new uint256[](0));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/// @notice A locked body keeps its equipped outfits until the lock expires, even if the owner also controls an
|
|
815
|
+
/// unlocked destination body.
|
|
816
|
+
function test_lock_preventsMovingOutfitFromLockedBody() public {
|
|
817
|
+
uint256[] memory outfits = new uint256[](1);
|
|
818
|
+
outfits[0] = NECKLACE_1;
|
|
819
|
+
|
|
820
|
+
vm.prank(alice);
|
|
821
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
822
|
+
|
|
823
|
+
vm.prank(alice);
|
|
824
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_A);
|
|
825
|
+
|
|
826
|
+
vm.prank(alice);
|
|
827
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
|
|
828
|
+
resolver.decorateBannyWith(address(hook), BODY_B, 0, outfits);
|
|
829
|
+
}
|
|
830
|
+
|
|
800
831
|
// =========================================================================
|
|
801
832
|
// SECTION 8: Complex Multi-Step Scenarios
|
|
802
833
|
// =========================================================================
|