@bannynet/core-v6 0.0.23 → 0.0.25
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/README.md +24 -27
- package/foundry.toml +2 -1
- package/package.json +22 -12
- package/src/Banny721TokenUriResolver.sol +6 -0
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -101
- package/AUDIT_INSTRUCTIONS.md +0 -75
- package/RISKS.md +0 -89
- package/SKILLS.md +0 -42
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -169
- package/foundry.lock +0 -14
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -521
- package/test/Banny721TokenUriResolver.t.sol +0 -694
- package/test/BannyAttacks.t.sol +0 -326
- package/test/DecorateFlow.t.sol +0 -1091
- package/test/Fork.t.sol +0 -2026
- package/test/OutfitTransferLifecycle.t.sol +0 -395
- package/test/TestAuditGaps.sol +0 -724
- package/test/TestQALastMile.t.sol +0 -447
- package/test/audit/AntiStrandingRetention.t.sol +0 -422
- package/test/audit/BurnedBodyStrandsAssets.t.sol +0 -163
- package/test/audit/DuplicateCategoryRetention.t.sol +0 -163
- package/test/audit/MergedOutfitExclusivity.t.sol +0 -228
- package/test/audit/MigrationHelperVerificationBypass.t.sol +0 -102
- package/test/audit/TryTransferFromStrandsAssets.t.sol +0 -197
- package/test/regression/ArrayLengthValidation.t.sol +0 -57
- package/test/regression/BodyCategoryValidation.t.sol +0 -147
- package/test/regression/BurnedTokenCheck.t.sol +0 -186
- package/test/regression/CEIReorder.t.sol +0 -209
- package/test/regression/ClearMetadata.t.sol +0 -52
- package/test/regression/MsgSenderEvents.t.sol +0 -153
- package/test/regression/RemovedTierDesync.t.sol +0 -346
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Banny Retail
|
|
2
2
|
|
|
3
|
-
Banny Retail is an
|
|
3
|
+
Banny Retail is an onchain avatar system for Juicebox 721 collections. A body NFT can wear outfit NFTs, use a background NFT, and resolve to a base64 JSON token URI whose image is an onchain SVG.
|
|
4
4
|
|
|
5
5
|
Docs: <https://docs.juicebox.money>
|
|
6
6
|
Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
|
|
@@ -12,34 +12,32 @@ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
|
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
14
14
|
|
|
15
|
-
This is a resolver-centric
|
|
15
|
+
This is a resolver-centric app built on top of [`@bananapus/721-hook-v6`](https://www.npmjs.com/package/@bananapus/721-hook-v6). The resolver holds attached outfit and background NFTs while a body is decorated, then composes the active layers into a single token URI response.
|
|
16
16
|
|
|
17
17
|
The main user flows are:
|
|
18
18
|
|
|
19
19
|
- mint body, outfit, and background NFTs through a Juicebox 721 hook
|
|
20
20
|
- attach accessories to a body with `decorateBannyWith`
|
|
21
|
-
- optionally freeze the
|
|
22
|
-
- upload SVG payloads
|
|
21
|
+
- optionally freeze the look for seven days with `lockOutfitChangesFor`
|
|
22
|
+
- upload SVG payloads after an owner registers the content hashes
|
|
23
23
|
|
|
24
|
-
Use this repo when you need collection-specific, fully
|
|
25
|
-
|
|
26
|
-
If a bug changes tier pricing, mint eligibility, or treasury flow, it is probably not here first. Start in the 721 hook repo and only come here once the issue is clearly in attachment, custody, or rendering behavior.
|
|
24
|
+
Use this repo when you need collection-specific, fully onchain metadata composition on top of Juicebox NFTs. Do not use it as a generic 721 hook. It is an app-layer resolver, not a protocol NFT primitive.
|
|
27
25
|
|
|
28
26
|
## Key Contract
|
|
29
27
|
|
|
30
28
|
| Contract | Role |
|
|
31
29
|
| --- | --- |
|
|
32
|
-
| `Banny721TokenUriResolver` | Resolves
|
|
30
|
+
| `Banny721TokenUriResolver` | Resolves metadata, stores equipped accessories, enforces outfit locks, and renders layered SVG output for Banny collections. |
|
|
33
31
|
|
|
34
32
|
## Mental Model
|
|
35
33
|
|
|
36
34
|
This repo owns three things:
|
|
37
35
|
|
|
38
|
-
1. custody of
|
|
39
|
-
2. rules
|
|
40
|
-
3. rendering of the final
|
|
36
|
+
1. custody of outfit and background NFTs while they are equipped
|
|
37
|
+
2. rules for what a body can wear and when that can change
|
|
38
|
+
3. rendering of the final metadata payload
|
|
41
39
|
|
|
42
|
-
It does not own mint pricing, tier issuance, or
|
|
40
|
+
It does not own mint pricing, tier issuance, or treasury accounting.
|
|
43
41
|
|
|
44
42
|
## Read These Files First
|
|
45
43
|
|
|
@@ -58,12 +56,11 @@ It does not own mint pricing, tier issuance, or project accounting.
|
|
|
58
56
|
|
|
59
57
|
## Integration Traps
|
|
60
58
|
|
|
61
|
-
- the resolver
|
|
62
|
-
- transferred bodies carry their equipped assets, so a new body holder can inherit control of
|
|
63
|
-
- burned bodies and non-safe transfer patterns can strand expectations around resolver-held assets
|
|
64
|
-
- outfit locks
|
|
65
|
-
- metadata quality depends on lazily uploaded asset payloads, not only
|
|
66
|
-
- collection logic here assumes a Juicebox 721 hook upstream and should not be read as a generic NFT renderer
|
|
59
|
+
- the resolver holds equipped assets, so transfer edge cases matter as much as rendering output
|
|
60
|
+
- transferred bodies carry their equipped assets, so a new body holder can inherit control of them
|
|
61
|
+
- burned bodies and non-safe transfer patterns can strand expectations around resolver-held assets
|
|
62
|
+
- outfit locks survive body transfers until expiry
|
|
63
|
+
- metadata quality depends on lazily uploaded asset payloads, not only token state
|
|
67
64
|
|
|
68
65
|
## Where State Lives
|
|
69
66
|
|
|
@@ -83,8 +80,8 @@ The contract stack relies on `via_ir = true` in `foundry.toml`.
|
|
|
83
80
|
|
|
84
81
|
```bash
|
|
85
82
|
npm install
|
|
86
|
-
forge build
|
|
87
|
-
forge test
|
|
83
|
+
forge build --deny notes
|
|
84
|
+
forge test --deny notes
|
|
88
85
|
```
|
|
89
86
|
|
|
90
87
|
Useful scripts:
|
|
@@ -96,7 +93,7 @@ Useful scripts:
|
|
|
96
93
|
|
|
97
94
|
## Deployment Notes
|
|
98
95
|
|
|
99
|
-
Deployments are handled through Sphinx using the environments configured in `script/Deploy.s.sol`. The resolver is
|
|
96
|
+
Deployments are handled through Sphinx using the environments configured in `script/Deploy.s.sol`. The resolver is meant to be plugged into a Juicebox 721 hook as that hook's token URI resolver.
|
|
100
97
|
|
|
101
98
|
## Repository Layout
|
|
102
99
|
|
|
@@ -115,14 +112,14 @@ script/
|
|
|
115
112
|
|
|
116
113
|
## Risks And Notes
|
|
117
114
|
|
|
118
|
-
- attached outfits and backgrounds are
|
|
115
|
+
- attached outfits and backgrounds are held by the resolver while equipped
|
|
119
116
|
- outfit locks are fixed-duration and cannot be shortened once set
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
- rendering quality
|
|
117
|
+
- onchain SVG content is immutable once uploaded for a committed hash
|
|
118
|
+
- plain `transferFrom` can still create asset-tracking surprises around resolver custody
|
|
119
|
+
- rendering quality depends on the integrity of uploaded SVG assets
|
|
123
120
|
|
|
124
121
|
## For AI Agents
|
|
125
122
|
|
|
126
|
-
- Treat this repo as an
|
|
123
|
+
- Treat this repo as an app-layer resolver, not as the NFT issuance primitive.
|
|
127
124
|
- Start with `Banny721TokenUriResolver` and the lifecycle tests before summarizing attachment behavior.
|
|
128
|
-
- If the question is about mint economics or tier availability, inspect `nana-721-hook-v6` instead
|
|
125
|
+
- If the question is about mint economics or tier availability, inspect `nana-721-hook-v6` instead.
|
package/foundry.toml
CHANGED
|
@@ -15,7 +15,8 @@ depth = 100
|
|
|
15
15
|
fail_on_revert = false
|
|
16
16
|
|
|
17
17
|
[lint]
|
|
18
|
-
exclude_lints = ["
|
|
18
|
+
exclude_lints = ["mixed-case-variable", "pascal-case-struct"]
|
|
19
|
+
|
|
19
20
|
[fmt]
|
|
20
21
|
number_underscore = "thousands"
|
|
21
22
|
multiline_func_header = "all"
|
package/package.json
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/mejango/banny-retail-v6"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"foundry.toml",
|
|
12
|
+
"references/",
|
|
13
|
+
"remappings.txt",
|
|
14
|
+
"script/Add.Denver.s.sol",
|
|
15
|
+
"script/Deploy.s.sol",
|
|
16
|
+
"script/Drop1.s.sol",
|
|
17
|
+
"script/helpers/",
|
|
18
|
+
"script/outfit_drop/",
|
|
19
|
+
"src/"
|
|
20
|
+
],
|
|
9
21
|
"engines": {
|
|
10
22
|
"node": ">=20.0.0"
|
|
11
23
|
},
|
|
@@ -20,18 +32,16 @@
|
|
|
20
32
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
|
|
21
33
|
},
|
|
22
34
|
"dependencies": {
|
|
23
|
-
"@bananapus/721-hook-v6": "
|
|
24
|
-
"@bananapus/core-v6": "
|
|
25
|
-
"@bananapus/
|
|
26
|
-
"@bananapus/
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
29
|
-
"
|
|
30
|
-
"@rev-net/core-v6": "^0.0.32",
|
|
31
|
-
"keccak": "^3.0.4"
|
|
35
|
+
"@bananapus/721-hook-v6": "0.0.43",
|
|
36
|
+
"@bananapus/core-v6": "0.0.39",
|
|
37
|
+
"@bananapus/router-terminal-v6": "0.0.36",
|
|
38
|
+
"@bananapus/suckers-v6": "0.0.33",
|
|
39
|
+
"@openzeppelin/contracts": "5.6.1",
|
|
40
|
+
"@rev-net/core-v6": "0.0.39",
|
|
41
|
+
"keccak": "3.0.4"
|
|
32
42
|
},
|
|
33
43
|
"devDependencies": {
|
|
34
|
-
"@bananapus/address-registry-v6": "
|
|
35
|
-
"@sphinx-labs/plugins": "
|
|
44
|
+
"@bananapus/address-registry-v6": "0.0.25",
|
|
45
|
+
"@sphinx-labs/plugins": "0.33.3"
|
|
36
46
|
}
|
|
37
47
|
}
|
|
@@ -291,6 +291,8 @@ contract Banny721TokenUriResolver is
|
|
|
291
291
|
|
|
292
292
|
// If the token has an owner, check if the owner has locked the token.
|
|
293
293
|
uint256 lockedUntil = outfitLockedUntil[hook][tokenId];
|
|
294
|
+
// Outfit locks are user-selected display locks; timestamp tolerance is acceptable here.
|
|
295
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
294
296
|
if (lockedUntil > block.timestamp) {
|
|
295
297
|
extraMetadata = string.concat(extraMetadata, '"changesLockedUntil": ', lockedUntil.toString(), ",");
|
|
296
298
|
attributes = string.concat(
|
|
@@ -945,6 +947,8 @@ contract Banny721TokenUriResolver is
|
|
|
945
947
|
/// @param bannyBodyId The body currently using the asset.
|
|
946
948
|
/// @param exemptBodyId The destination body currently being decorated.
|
|
947
949
|
function _revertIfBodyLocked(address hook, uint256 bannyBodyId, uint256 exemptBodyId) internal view {
|
|
950
|
+
// Outfit locks are user-selected display locks; timestamp tolerance is acceptable here.
|
|
951
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
948
952
|
if (bannyBodyId != 0 && bannyBodyId != exemptBodyId && outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
|
|
949
953
|
revert Banny721TokenUriResolver_OutfitChangesLocked();
|
|
950
954
|
}
|
|
@@ -1118,6 +1122,8 @@ contract Banny721TokenUriResolver is
|
|
|
1118
1122
|
}
|
|
1119
1123
|
|
|
1120
1124
|
// Can't decorate a banny that's locked.
|
|
1125
|
+
// Outfit locks are user-selected display locks; timestamp tolerance is acceptable here.
|
|
1126
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
1121
1127
|
if (outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
|
|
1122
1128
|
revert Banny721TokenUriResolver_OutfitChangesLocked();
|
|
1123
1129
|
}
|
package/ADMINISTRATION.md
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Administration
|
|
2
|
-
|
|
3
|
-
## At A Glance
|
|
4
|
-
|
|
5
|
-
| Item | Details |
|
|
6
|
-
| --- | --- |
|
|
7
|
-
| Scope | `Banny721TokenUriResolver` metadata, SVG commitments, and outfit-state control |
|
|
8
|
-
| Control posture | Global `Ownable` metadata control plus per-body owner control |
|
|
9
|
-
| Highest-risk actions | Wrong SVG hash commitments, incorrect metadata updates, and long outfit locks |
|
|
10
|
-
| Recovery posture | Metadata is editable, but committed hashes, uploaded SVGs, and active locks are not reversible |
|
|
11
|
-
|
|
12
|
-
## Purpose
|
|
13
|
-
|
|
14
|
-
`banny-retail-v6` has a small but real control plane. The resolver owner controls collection-wide metadata and SVG commitments. Body owners control decoration and outfit locks. No admin can rescue equipped NFTs from custody if resolver logic fails.
|
|
15
|
-
|
|
16
|
-
## Control Model
|
|
17
|
-
|
|
18
|
-
- `Banny721TokenUriResolver` is `Ownable`.
|
|
19
|
-
- Global admin power is limited to metadata, product naming, and SVG hash commitments.
|
|
20
|
-
- Actual SVG payload upload is permissionless once the hash is committed.
|
|
21
|
-
- Body owners control decoration and locking for their own bodies.
|
|
22
|
-
- Equipped accessories are held custodially by the resolver while attached.
|
|
23
|
-
|
|
24
|
-
## Roles
|
|
25
|
-
|
|
26
|
-
| Role | How Assigned | Scope | Notes |
|
|
27
|
-
| --- | --- | --- | --- |
|
|
28
|
-
| Resolver owner | `Ownable(owner)` at construction | Global | Can transfer ownership with OpenZeppelin `transferOwnership()` |
|
|
29
|
-
| Body owner | `IERC721(hook).ownerOf(bannyBodyId)` | Per body | Can decorate and lock that body |
|
|
30
|
-
| Anyone | No assignment | Global | Can upload SVG bytes only if they match a committed hash |
|
|
31
|
-
|
|
32
|
-
## Privileged Surfaces
|
|
33
|
-
|
|
34
|
-
| Contract | Function | Who Can Call | Effect |
|
|
35
|
-
| --- | --- | --- | --- |
|
|
36
|
-
| `Banny721TokenUriResolver` | `setMetadata(...)` | Resolver owner | Changes global description, URL, and base URI |
|
|
37
|
-
| `Banny721TokenUriResolver` | `setProductNames(...)` | Resolver owner | Changes display names for products |
|
|
38
|
-
| `Banny721TokenUriResolver` | `setSvgHashesOf(...)` | Resolver owner | Commits write-once SVG hashes for UPCs |
|
|
39
|
-
| `Banny721TokenUriResolver` | `setSvgContentsOf(...)` | Anyone with matching bytes | Uploads write-once SVG payloads for committed hashes |
|
|
40
|
-
| `Banny721TokenUriResolver` | `decorateBannyWith(...)` | Current body owner | Equips or unequips accessories and updates custody |
|
|
41
|
-
| `Banny721TokenUriResolver` | `lockOutfitChangesFor(...)` | Current body owner | Extends the outfit lock window for that body |
|
|
42
|
-
|
|
43
|
-
## Immutable And One-Way
|
|
44
|
-
|
|
45
|
-
- SVG hash commitments are write-once.
|
|
46
|
-
- SVG contents are write-once once uploaded.
|
|
47
|
-
- `lockOutfitChangesFor(...)` only extends the active lock; it never shortens it.
|
|
48
|
-
- The lock duration is fixed by the `_LOCK_DURATION` constant.
|
|
49
|
-
- Default art fragments, category semantics, and the trusted forwarder are constructor or code immutables.
|
|
50
|
-
|
|
51
|
-
## Operational Notes
|
|
52
|
-
|
|
53
|
-
- Treat `setSvgHashesOf(...)` like a release gate. A wrong hash usually means new resolver or new UPC strategy, not an edit.
|
|
54
|
-
- Treat `setMetadata(...)` and `setProductNames(...)` as collection-wide display changes.
|
|
55
|
-
- Remind users that equipped assets are in resolver custody while attached.
|
|
56
|
-
- Only lock outfits when temporary non-editability is the intended user experience.
|
|
57
|
-
- Use safe ERC-721 transfer flows when assets are sent into the resolver path; plain `transferFrom` can bypass receiver checks and strand NFTs without a recovery path.
|
|
58
|
-
|
|
59
|
-
## Machine Notes
|
|
60
|
-
|
|
61
|
-
- Do not infer a rescue path for equipped assets; there is none in this contract.
|
|
62
|
-
- Treat `src/Banny721TokenUriResolver.sol` as the source of truth for lock extension and write-once SVG behavior.
|
|
63
|
-
- If a committed hash and intended asset bytes differ, stop; the contract does not support overwrite repair.
|
|
64
|
-
- If an asset arrived through non-safe ERC-721 transfer semantics, do not assume the resolver can detect or recover it.
|
|
65
|
-
|
|
66
|
-
## Recovery
|
|
67
|
-
|
|
68
|
-
- Bad metadata can be changed by the owner.
|
|
69
|
-
- Bad SVG commitments or uploaded content cannot be corrected in place.
|
|
70
|
-
- If equipped assets become stuck because of resolver logic, there is no owner rescue path in this contract.
|
|
71
|
-
- If NFTs are stranded through non-safe transfer semantics, this contract does not provide a recovery flow.
|
|
72
|
-
|
|
73
|
-
## Admin Boundaries
|
|
74
|
-
|
|
75
|
-
- The owner cannot withdraw equipped user NFTs arbitrarily.
|
|
76
|
-
- The owner cannot overwrite committed hashes or uploaded SVG contents.
|
|
77
|
-
- The owner cannot bypass body-owner checks on decoration or locking.
|
|
78
|
-
- Nobody can shorten an active outfit lock.
|
|
79
|
-
- There is no pause, upgrade, or rescue mechanism.
|
|
80
|
-
|
|
81
|
-
## Source Map
|
|
82
|
-
|
|
83
|
-
- `src/Banny721TokenUriResolver.sol`
|
|
84
|
-
- `src/interfaces/IBanny721TokenUriResolver.sol`
|
|
85
|
-
- `script/Deploy.s.sol`
|
|
86
|
-
- `test/TestAuditGaps.sol`
|
|
87
|
-
- `test/TestQALastMile.t.sol`
|
package/ARCHITECTURE.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
`banny-retail-v6` is the Banny-specific metadata and attachment layer for Juicebox 721 collections. It does not mint the NFTs or own treasury logic. It owns attachment custody, outfit-lock rules, and final token rendering.
|
|
6
|
-
|
|
7
|
-
## System Overview
|
|
8
|
-
|
|
9
|
-
The repo is centered on `Banny721TokenUriResolver`. A 721 hook from `nana-721-hook-v6` points to this resolver for `tokenURI(...)`, while bodies, outfits, and backgrounds remain separate NFTs at the collection layer. The resolver escrows equipped accessories, records which assets are attached to each body, and composes the final SVG and JSON metadata on demand.
|
|
10
|
-
|
|
11
|
-
## Core Invariants
|
|
12
|
-
|
|
13
|
-
- A body can only reference accessories that are currently escrowed by the resolver.
|
|
14
|
-
- Replacing an equipped item must atomically return the old item and escrow the new item.
|
|
15
|
-
- Outfit locks must block both explicit removal and implicit replacement until the lock expires.
|
|
16
|
-
- Equipped assets travel with the body NFT on transfer until the new owner unequips them.
|
|
17
|
-
- Registered SVG payloads must match their pre-registered content hash before they become renderable.
|
|
18
|
-
- Rendering must stay deterministic for the same stored body state.
|
|
19
|
-
|
|
20
|
-
## Modules
|
|
21
|
-
|
|
22
|
-
| Module | Responsibility | Notes |
|
|
23
|
-
| --- | --- | --- |
|
|
24
|
-
| `Banny721TokenUriResolver` | Escrow, attachment state, lock windows, and metadata rendering | Main contract; application-specific |
|
|
25
|
-
| `IBanny721TokenUriResolver` | External integration surface | Used by hooks and offchain tooling |
|
|
26
|
-
|
|
27
|
-
## Trust Boundaries
|
|
28
|
-
|
|
29
|
-
- Minting, ownership transfer, and collection-level ERC-721 semantics live in `nana-721-hook-v6`.
|
|
30
|
-
- This repo is trusted for rendering correctness and custody of equipped assets.
|
|
31
|
-
- Asset content upload is controlled by the registered content owner, but the contract verifies the uploaded bytes against the stored hash.
|
|
32
|
-
|
|
33
|
-
## Critical Flows
|
|
34
|
-
|
|
35
|
-
### Decorate
|
|
36
|
-
|
|
37
|
-
```text
|
|
38
|
-
body owner
|
|
39
|
-
-> calls decorateBannyWith(...)
|
|
40
|
-
-> resolver verifies body ownership and lock status
|
|
41
|
-
-> resolver pulls new accessories into escrow
|
|
42
|
-
-> resolver updates equipped slots
|
|
43
|
-
-> resolver returns replaced accessories to the owner
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Render
|
|
47
|
-
|
|
48
|
-
```text
|
|
49
|
-
tokenURI(bodyId)
|
|
50
|
-
-> resolver loads body, background, and equipped slot state
|
|
51
|
-
-> fetches registered SVG fragments
|
|
52
|
-
-> composes layered SVG in Banny-specific order
|
|
53
|
-
-> returns base64 JSON metadata
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Lock Outfit
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
body owner
|
|
60
|
-
-> calls lockOutfitChangesFor(...)
|
|
61
|
-
-> resolver stores a no-change window
|
|
62
|
-
-> later decoration and removal paths must respect it
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Accounting Model
|
|
66
|
-
|
|
67
|
-
This repo does not own treasury accounting. Its critical state is custody accounting: which NFTs are escrowed, which body they belong to, and when a body is locked against changes.
|
|
68
|
-
|
|
69
|
-
That custody model uses lazy reconciliation for some stale attachment records. Read paths filter against current ownership and attachment state instead of eagerly rewriting storage on every external transfer.
|
|
70
|
-
|
|
71
|
-
## Security Model
|
|
72
|
-
|
|
73
|
-
- The main failure mode is custody drift between slot state and actual escrowed NFTs.
|
|
74
|
-
- Rendering order is part of application semantics, not cosmetic output.
|
|
75
|
-
- Lazy reconciliation is intentional. Changes that assume attachment arrays are perfectly clean in storage can strand assets or mis-render bodies.
|
|
76
|
-
- Any new asset category adds both a rendering concern and a custody concern.
|
|
77
|
-
|
|
78
|
-
## Safe Change Guide
|
|
79
|
-
|
|
80
|
-
- Keep generic ERC-721 behavior in `nana-721-hook-v6`, not here.
|
|
81
|
-
- Review escrow writes and transfer behavior together whenever changing attachment logic.
|
|
82
|
-
- If transfer or cleanup behavior changes, re-check lazy reconciliation assumptions alongside body-transfer inheritance of equipped assets.
|
|
83
|
-
- If `tokenURI(...)` changes, test stable output for unchanged state and replacement behavior for changed state.
|
|
84
|
-
- If adding slots or asset classes, update rendering order, slot replacement, and lock enforcement in one change.
|
|
85
|
-
|
|
86
|
-
## Canonical Checks
|
|
87
|
-
|
|
88
|
-
- accessory escrow, replacement, and decoration flow:
|
|
89
|
-
`test/DecorateFlow.t.sol`
|
|
90
|
-
- burned-body custody edge cases:
|
|
91
|
-
`test/audit/BurnedBodyStrandsAssets.t.sol`
|
|
92
|
-
- transfer-path protection against stranded attachments:
|
|
93
|
-
`test/audit/TryTransferFromStrandsAssets.t.sol`
|
|
94
|
-
|
|
95
|
-
## Source Map
|
|
96
|
-
|
|
97
|
-
- `src/Banny721TokenUriResolver.sol`
|
|
98
|
-
- `test/DecorateFlow.t.sol`
|
|
99
|
-
- `test/audit/BurnedBodyStrandsAssets.t.sol`
|
|
100
|
-
- `test/audit/TryTransferFromStrandsAssets.t.sol`
|
|
101
|
-
- `script/Deploy.s.sol`
|
package/AUDIT_INSTRUCTIONS.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Audit Instructions
|
|
2
|
-
|
|
3
|
-
This repo is the Banny avatar composition layer. It does not mint the base NFTs, but it does custody equipped accessories and define the metadata users see.
|
|
4
|
-
|
|
5
|
-
## Audit Objective
|
|
6
|
-
|
|
7
|
-
Find issues that:
|
|
8
|
-
- strand outfits or backgrounds in resolver custody
|
|
9
|
-
- let the wrong actor equip, unequip, overwrite, or recover accessories
|
|
10
|
-
- break outfit-lock timing or freeze a body longer than intended
|
|
11
|
-
- return metadata that does not match stored attachment state
|
|
12
|
-
- bypass category or layering constraints
|
|
13
|
-
|
|
14
|
-
## Scope
|
|
15
|
-
|
|
16
|
-
In scope:
|
|
17
|
-
- `src/Banny721TokenUriResolver.sol`
|
|
18
|
-
- `src/interfaces/IBanny721TokenUriResolver.sol`
|
|
19
|
-
- all deployment helpers in `script/`
|
|
20
|
-
|
|
21
|
-
## Start Here
|
|
22
|
-
|
|
23
|
-
1. `src/Banny721TokenUriResolver.sol`
|
|
24
|
-
2. accessory receipt and release paths
|
|
25
|
-
3. deployment wiring in `script/`
|
|
26
|
-
|
|
27
|
-
## Security Model
|
|
28
|
-
|
|
29
|
-
The resolver is an attachment and rendering layer around a `JB721TiersHook` collection.
|
|
30
|
-
- the underlying 721 hook remains the token contract and source of body ownership
|
|
31
|
-
- the resolver temporarily holds accessory NFTs while they are equipped
|
|
32
|
-
- body ownership should be the only authority that changes equipped state
|
|
33
|
-
- accessory contracts may be hostile or malformed, so receipt and release ordering matters
|
|
34
|
-
|
|
35
|
-
## Roles And Privileges
|
|
36
|
-
|
|
37
|
-
| Role | Powers | How constrained |
|
|
38
|
-
|------|--------|-----------------|
|
|
39
|
-
| Body owner | Equip, unequip, and lock accessories | Must be derived from the current hook-reported owner |
|
|
40
|
-
| Resolver owner | Update metadata and SVG-related admin state | Must not control equipped-state authorization |
|
|
41
|
-
| Accessory NFT contract | Execute callbacks during custody changes | Must not corrupt bookkeeping or steal custody |
|
|
42
|
-
|
|
43
|
-
## Integration Assumptions
|
|
44
|
-
|
|
45
|
-
| Dependency | Assumption | What breaks if wrong |
|
|
46
|
-
|------------|------------|----------------------|
|
|
47
|
-
| `JB721TiersHook` | Reports authentic body ownership and tier metadata | Unauthorized decoration or incorrect rendering |
|
|
48
|
-
| Accessory ERC-721s | Behave like standard transferable NFTs | Custody or release flows fail unexpectedly |
|
|
49
|
-
|
|
50
|
-
## Critical Invariants
|
|
51
|
-
|
|
52
|
-
1. Every accessory transferred into the resolver remains attributable to one body or is recoverable by the rightful owner.
|
|
53
|
-
2. Only the current body owner or an intended delegate can change that body's equipped state.
|
|
54
|
-
3. Conflicting categories cannot be equipped together, including through replacement or invalidation edge paths.
|
|
55
|
-
4. Outfit-lock state only affects the intended body for the intended duration.
|
|
56
|
-
5. Metadata and SVG generation reflect current state and do not expose impossible combinations.
|
|
57
|
-
|
|
58
|
-
## Attack Surfaces
|
|
59
|
-
|
|
60
|
-
- decoration entrypoints that replace one accessory with another
|
|
61
|
-
- ERC-721 receipt hooks and any path that accepts custody
|
|
62
|
-
- release paths after redecorating, burning, or invalid token state
|
|
63
|
-
- category validation and conflict checks
|
|
64
|
-
- metadata assembly that assumes on-chain assets or tier data remain available
|
|
65
|
-
|
|
66
|
-
## Accepted Risks Or Behaviors
|
|
67
|
-
|
|
68
|
-
- Equipped accessories intentionally follow the body unless they are unequipped first.
|
|
69
|
-
- Preserving attribution on failed transfer-out is safer than dropping custody state.
|
|
70
|
-
|
|
71
|
-
## Verification
|
|
72
|
-
|
|
73
|
-
- `npm install`
|
|
74
|
-
- `forge build`
|
|
75
|
-
- `forge test`
|
package/RISKS.md
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Banny Retail Risk Register
|
|
2
|
-
|
|
3
|
-
This file focuses on failure modes that can break NFT custody, let untrusted hook integrations bypass assumptions, or leave a rendered Banny in a state that does not match the assets users think they own.
|
|
4
|
-
|
|
5
|
-
## How to use this file
|
|
6
|
-
|
|
7
|
-
- Read `Priority risks` first; those are the highest-signal failure modes for operators, auditors, and integrators.
|
|
8
|
-
- Use `Accepted Behaviors` to separate intentional tradeoffs from genuine bugs.
|
|
9
|
-
- Treat `Invariants to Verify` as required test and audit targets.
|
|
10
|
-
|
|
11
|
-
## Priority risks
|
|
12
|
-
|
|
13
|
-
| Priority | Risk | Why it matters | Primary controls |
|
|
14
|
-
|----------|------|----------------|------------------|
|
|
15
|
-
| P0 | Untrusted `hook` or store integration | The caller chooses the hook, and the resolver trusts it for ownership checks, tier metadata, and transfers. A bad hook can fake authority or trap assets. | Operationally restrict supported hooks, scrutinize sections 1, 3, and 5, and test with hostile hook behavior. |
|
|
16
|
-
| P1 | Silent transfer failure retention | Failed returns intentionally keep attachment records to avoid stranding NFTs, but this can leave phantom render state if the underlying asset is gone forever. | Explicit accepted-behavior rules, retained-item handling, and invariants around custody/state correspondence. |
|
|
17
|
-
| P1 | Sale-time outfit lock griefing | A seller can transfer a locked body and force the buyer to wait up to 7 days before changing outfits. | Fixed-duration lock, marketplace disclosure, and user education before secondary sales. |
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## 1. Trust Assumptions
|
|
21
|
-
|
|
22
|
-
- **Trusted forwarder.** ERC-2771 `_msgSender()` is trusted for all ownership checks in `decorateBannyWith`, `lockOutfitChangesFor`, and admin functions. A compromised forwarder can dress/undress any banny and steal equipped outfits.
|
|
23
|
-
- **Hook contract.** The `hook` parameter is caller-supplied and not validated against any registry. A malicious hook contract could return arbitrary tier data, manipulate ownership checks, or trap NFTs.
|
|
24
|
-
- **Owner (Ownable).** The contract owner controls SVG hashes, product names, and metadata URIs. A compromised owner can set malicious SVG content hashes, enabling XSS via on-chain SVG injection after the matching content is uploaded.
|
|
25
|
-
- **721 hook store.** `_storeOf(hook)` calls `IJB721TiersHook(hook).STORE()` -- trusts the hook to return a legitimate store. A malicious hook can return a fake store with manipulated tier data.
|
|
26
|
-
|
|
27
|
-
## 2. Economic / Manipulation Risks
|
|
28
|
-
|
|
29
|
-
- **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.
|
|
30
|
-
- **try-catch silent failures with retention.** `_tryTransferFrom` silently catches all transfer failures and returns `false`. When a transfer fails, the resolver preserves the attachment record instead of clearing state. For backgrounds, the entire background change is aborted. For outfits, failed-to-return items are retained in the attached list via `_storeOutfitsWithRetained`. This prevents NFT stranding — assets remain tracked and recoverable once the transfer issue is resolved (e.g., the owner contract becomes receivable). However, if an outfit NFT is burned or its tier removed, the retained record refers to a non-existent asset, creating a phantom entry in the SVG rendering.
|
|
31
|
-
- **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.
|
|
32
|
-
|
|
33
|
-
## 3. Access Control
|
|
34
|
-
|
|
35
|
-
- **No hook validation (HIGH impact).** Any address can be passed as `hook`. A malicious hook can return `_msgSender()` from `ownerOf()` to pass authorization checks, execute arbitrary code during `safeTransferFrom`, or return manipulated tier data from `STORE().tierOfTokenId()`.
|
|
36
|
-
- **SVG content upload is permissionless (with hash).** `setSvgContentsOf` only requires the content to match a pre-committed hash. Safe if hashes are correctly committed.
|
|
37
|
-
- **onERC721Received restriction.** Only accepts NFTs when `operator == address(this)`. `transferFrom` (non-safe) bypasses this -- NFTs sent via `transferFrom` are permanently locked with no rescue function.
|
|
38
|
-
|
|
39
|
-
## 4. DoS Vectors
|
|
40
|
-
|
|
41
|
-
- **External call iteration scales with outfit count.** `_attachedOutfitIdsOf[hook][bannyBodyId]` is replaced wholesale on each `decorateBannyWith` call (not appended to), so the array is bounded by the number of currently equipped outfits, not cumulative history. However, `decorateBannyWith` iterates over both the previous and new outfit arrays to diff them (transferring removed outfits back and new outfits in), so gas cost scales with the number of outfits being equipped/unequipped in a single call.
|
|
42
|
-
- **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times per outfit. A malicious hook that consumes excessive gas or reverts can make token metadata unretrievable. Measured: `tokenUriOf` with a well-behaved hook and 9 equipped outfits costs ~609k gas (see `test_tokenUri_gasSnapshot_9outfits`). The practical ceiling for a malicious hook is bounded only by the caller's gas limit — RPC nodes typically cap `eth_call` at 30M+ gas, so even expensive hooks won't fail for off-chain reads, but on-chain consumers (e.g., other contracts calling `tokenURI`) could revert.
|
|
43
|
-
|
|
44
|
-
## 5. Integration Risks
|
|
45
|
-
|
|
46
|
-
- **Cross-contract NFT custody.** Outfits are held by `Banny721TokenUriResolver` via `safeTransferFrom`. If approval is revoked on the hook contract, equipping fails.
|
|
47
|
-
- **Tier removal desync.** If a tier is removed from the 721 hook while an outfit from that tier is equipped, `_productOfTokenId` returns a product with `id == 0`. The outfit remains equipped but renders as empty. `_tryTransferFrom` may fail silently when trying to return it.
|
|
48
|
-
- **Non-safe transfer loss.** Outfits sent directly to this contract via `transferFrom` (not `safeTransferFrom`) are permanently stuck since there is no rescue function.
|
|
49
|
-
- **ReentrancyGuard.** `decorateBannyWith` uses `nonReentrant`, but `lockOutfitChangesFor` and view functions do not. Reentrancy through hook callbacks is possible but state updates follow CEI pattern.
|
|
50
|
-
|
|
51
|
-
## 6. Invariants to Verify
|
|
52
|
-
|
|
53
|
-
- Every outfit held by this contract has a corresponding `_wearerOf[hook][outfitId]` pointing to a valid banny body.
|
|
54
|
-
- Every background held by this contract has a corresponding `_userOf[hook][backgroundId]` pointing to a valid banny body.
|
|
55
|
-
- `outfitLockedUntil[hook][bannyBodyId]` is monotonically non-decreasing per banny body (lock can only be extended, never shortened).
|
|
56
|
-
- After `decorateBannyWith`, all previously equipped outfits not in the new set are either transferred back to `_msgSender()` or retained in the attached list if the transfer failed.
|
|
57
|
-
- `_attachedOutfitIdsOf[hook][bannyBodyId]` contains the outfitIds passed to the most recent `decorateBannyWith` call, plus any retained outfits whose return transfer failed. Category exclusivity is enforced on the merged set (retained + new outfits), not just the new outfit set alone. Additionally, duplicate categories in the merged set are rejected with `Banny721TokenUriResolver_DuplicateCategory()` to prevent retained outfits from silently duplicating a category supplied in the new set.
|
|
58
|
-
- SVG content integrity: `keccak256(_svgContentOf[upc]) == svgHashOf[upc]` for all populated entries.
|
|
59
|
-
- NFT custody balance: the number of outfit NFTs held by this contract (`IERC721(hook).balanceOf(address(this))`) equals the total number of outfits currently equipped across all banny bodies for that hook. Violations indicate phantom outfits (equipped in state but NFT lost via try-catch silent failure) or orphaned NFTs (held by contract but not tracked in `_wearerOf`).
|
|
60
|
-
|
|
61
|
-
## 7. Accepted Behaviors
|
|
62
|
-
|
|
63
|
-
### 7.1 Failed transfers retain attachment records (anti-stranding)
|
|
64
|
-
|
|
65
|
-
`_tryTransferFrom` catches all transfer failures and returns `false`. When returning a previously equipped item fails, the resolver preserves the attachment record rather than clearing state:
|
|
66
|
-
|
|
67
|
-
- **Backgrounds**: If returning the old background fails, the entire background change is aborted (`return` in `_decorateBannyWithBackground`). The old background stays attached and the new one is not equipped.
|
|
68
|
-
- **Background removal**: If returning the background fails during removal (backgroundId=0), `_attachedBackgroundIdOf` is not cleared. The background stays attached.
|
|
69
|
-
- **Outfits**: Failed-to-return outfits remain non-zero in the `previousOutfitIds` array. `_storeOutfitsWithRetained` appends them to the new outfit list, preserving their attachment record. After merging, the resolver verifies no two outfits share the same category (reverts with `DuplicateCategory` if a retained outfit conflicts with a newly supplied one).
|
|
70
|
-
|
|
71
|
-
This prevents NFT stranding — assets held by the resolver stay tracked and recoverable. Once the transfer issue is resolved (e.g., the owner contract implements `IERC721Receiver`), a subsequent `decorateBannyWith` call will successfully return the retained items.
|
|
72
|
-
|
|
73
|
-
For permanently unrecoverable assets (burned NFTs, removed tiers), the retained record creates a phantom entry in the SVG rendering and attached list. This is cosmetically incorrect but not economically exploitable — phantom entries cannot be transferred or sold. The alternative — reverting on any failed transfer — would make `decorateBannyWith` fragile: a single burned outfit would prevent the banny owner from changing ANY outfits.
|
|
74
|
-
|
|
75
|
-
### 7.2 Lock griefing window is bounded at 7 days
|
|
76
|
-
|
|
77
|
-
`lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. A seller who locks just before transferring the banny forces the buyer to wait up to 7 days. This is accepted because: (1) marketplaces can check `outfitLockedUntil` before displaying the item, (2) the lock duration is fixed (not owner-configurable), and (3) the lock prevents a more severe attack where a buyer immediately strips valuable outfits — the lock gives the previous owner time to arrange the sale intentionally.
|
|
78
|
-
|
|
79
|
-
### 7.3 On-chain SVG rendering gas is well within limits
|
|
80
|
-
|
|
81
|
-
`tokenUriOf` constructs full SVGs on-chain with string concatenation. Measured gas ceiling: ~609K gas for the worst case (9 non-conflicting outfits + background with on-chain SVG content), well within typical RPC node limits (30M+). Regression test: `test_tokenUri_gasSnapshot_9outfits` in `test/TestQALastMile.t.sol`.
|
|
82
|
-
|
|
83
|
-
### 7.4 Outfits burn alongside the body
|
|
84
|
-
|
|
85
|
-
When a banny body NFT is burned (e.g. via cash-out), any equipped outfits and backgrounds held by the resolver are permanently unrecoverable. The resolver has no recovery function and this is intentional — outfits are part of the body's identity and share its fate. Users who want to preserve outfits must unequip them before burning the body.
|
|
86
|
-
|
|
87
|
-
### 7.5 Reentrancy in non-guarded functions is harmless
|
|
88
|
-
|
|
89
|
-
`lockOutfitChangesFor` and all view functions (`tokenUriOf`, `svgOf`) are not protected by `nonReentrant`. A malicious hook's `STORE().tierOfTokenId()` could re-enter `lockOutfitChangesFor` during a `tokenUriOf` call, but this is harmless -- `lockOutfitChangesFor` only extends the lock timestamp (monotonically non-decreasing) and has no state that could be corrupted by reentrancy. The view functions themselves are read-only at the contract level (no storage writes), so reentrancy through them cannot extract value.
|
package/SKILLS.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Banny Retail
|
|
2
|
-
|
|
3
|
-
## Use This File For
|
|
4
|
-
|
|
5
|
-
- Use this file when the task involves Banny outfit attachment, layered SVG rendering, token URI composition, or asset custody and lock behavior.
|
|
6
|
-
- Start here, then decide whether the issue is custody state, lock semantics, stored SVG content, or final token-URI composition. Those problems often look similar from the outside.
|
|
7
|
-
|
|
8
|
-
## Read This Next
|
|
9
|
-
|
|
10
|
-
| If you need... | Open this next |
|
|
11
|
-
|---|---|
|
|
12
|
-
| Repo overview and user-facing behavior | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
|
|
13
|
-
| Resolver implementation | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
|
|
14
|
-
| Runtime and content-management invariants | [`references/runtime.md`](./references/runtime.md), [`references/operations.md`](./references/operations.md) |
|
|
15
|
-
| Deployment or scripted drops | [`script/Deploy.s.sol`](./script/Deploy.s.sol), [`script/Drop1.s.sol`](./script/Drop1.s.sol), [`script/Add.Denver.s.sol`](./script/Add.Denver.s.sol) |
|
|
16
|
-
| Decoration lifecycle and custody invariants | [`test/DecorateFlow.t.sol`](./test/DecorateFlow.t.sol), [`test/OutfitTransferLifecycle.t.sol`](./test/OutfitTransferLifecycle.t.sol) |
|
|
17
|
-
| Adversarial, fork, or final QA coverage | [`test/BannyAttacks.t.sol`](./test/BannyAttacks.t.sol), [`test/Fork.t.sol`](./test/Fork.t.sol), [`test/TestAuditGaps.sol`](./test/TestAuditGaps.sol), [`test/TestQALastMile.t.sol`](./test/TestQALastMile.t.sol) |
|
|
18
|
-
|
|
19
|
-
## Repo Map
|
|
20
|
-
|
|
21
|
-
| Area | Where to look |
|
|
22
|
-
|---|---|
|
|
23
|
-
| Main contract | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
|
|
24
|
-
| Scripts | [`script/`](./script/) |
|
|
25
|
-
| Tests | [`test/`](./test/) |
|
|
26
|
-
|
|
27
|
-
## Purpose
|
|
28
|
-
|
|
29
|
-
Application-layer token URI resolver for Juicebox 721 collections that lets Banny body NFTs equip outfit and background NFTs, custody them while equipped, and render fully on-chain layered SVG metadata.
|
|
30
|
-
|
|
31
|
-
## Reference Files
|
|
32
|
-
|
|
33
|
-
- Open [`references/runtime.md`](./references/runtime.md) when you need attachment and custody behavior, rendering order, or the main invariants that protect equipped assets.
|
|
34
|
-
- Open [`references/operations.md`](./references/operations.md) when you need upload and metadata-management behavior, deployment breadcrumbs, or the common stale-data traps around SVG content and scripts.
|
|
35
|
-
|
|
36
|
-
## Working Rules
|
|
37
|
-
|
|
38
|
-
- Start in [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) for both rendering and attachment behavior. This repo is mostly one contract with several tightly coupled responsibilities.
|
|
39
|
-
- Treat custody, stale attachment cleanup, and lock timing as high-risk. Rendering bugs are visible, but custody bugs are worse.
|
|
40
|
-
- Equipped outfits and backgrounds travel with the body NFT. Treat that inheritance behavior as intentional before calling it a custody bug.
|
|
41
|
-
- When a task mentions minting, pricing, or terminal accounting, verify that the problem is not actually in the upstream 721 hook repo.
|
|
42
|
-
- If you touch SVG or metadata behavior, check whether the issue is in stored content, rendering composition, or the hook-to-resolver integration point before patching.
|