@bannynet/core-v6 0.0.28 → 0.0.30
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 +3 -3
- package/package.json +6 -6
- package/references/operations.md +1 -1
- package/references/runtime.md +1 -1
- package/script/Add.Denver.s.sol +7 -0
- package/script/Deploy.s.sol +5 -5
- package/script/Drop1.s.sol +11 -0
- package/src/Banny721TokenUriResolver.sol +130 -56
package/README.md
CHANGED
|
@@ -50,8 +50,8 @@ It does not own mint pricing, tier issuance, or treasury accounting.
|
|
|
50
50
|
|
|
51
51
|
1. `test/DecorateFlow.t.sol`
|
|
52
52
|
2. `test/OutfitTransferLifecycle.t.sol`
|
|
53
|
-
3. `test/
|
|
54
|
-
4. `test/
|
|
53
|
+
3. `test/regression/BurnedBodyStrandsAssets.t.sol`
|
|
54
|
+
4. `test/regression/TryTransferFromStrandsAssets.t.sol`
|
|
55
55
|
5. `test/TestQALastMile.t.sol`
|
|
56
56
|
|
|
57
57
|
## Integration Traps
|
|
@@ -102,7 +102,7 @@ src/
|
|
|
102
102
|
Banny721TokenUriResolver.sol
|
|
103
103
|
interfaces/
|
|
104
104
|
test/
|
|
105
|
-
unit, attack, fork,
|
|
105
|
+
unit, attack, fork, review, QA, and regression coverage
|
|
106
106
|
script/
|
|
107
107
|
Deploy.s.sol
|
|
108
108
|
Drop1.s.sol
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@bananapus/721-hook-v6": "0.0.43",
|
|
36
|
-
"@bananapus/core-v6": "0.0.
|
|
37
|
-
"@bananapus/router-terminal-v6": "0.0.36",
|
|
38
|
-
"@bananapus/suckers-v6": "0.0.33",
|
|
35
|
+
"@bananapus/721-hook-v6": "^0.0.43",
|
|
36
|
+
"@bananapus/core-v6": "^0.0.48",
|
|
37
|
+
"@bananapus/router-terminal-v6": "^0.0.36",
|
|
38
|
+
"@bananapus/suckers-v6": "^0.0.33",
|
|
39
39
|
"@openzeppelin/contracts": "5.6.1",
|
|
40
|
-
"@rev-net/core-v6": "0.0.39",
|
|
40
|
+
"@rev-net/core-v6": "^0.0.39",
|
|
41
41
|
"keccak": "3.0.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/references/operations.md
CHANGED
|
@@ -21,5 +21,5 @@
|
|
|
21
21
|
|
|
22
22
|
## Useful Proof Points
|
|
23
23
|
|
|
24
|
-
- [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) and [`test/
|
|
24
|
+
- [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for security-sensitive assumptions.
|
|
25
25
|
- [`script/Drop1.s.sol`](../script/Drop1.s.sol) and [`script/Add.Denver.s.sol`](../script/Add.Denver.s.sol) when a deployment issue is really a script/config problem.
|
package/references/runtime.md
CHANGED
|
@@ -24,4 +24,4 @@
|
|
|
24
24
|
- [`test/DecorateFlow.t.sol`](../test/DecorateFlow.t.sol) for the main equip/unequip lifecycle.
|
|
25
25
|
- [`test/OutfitTransferLifecycle.t.sol`](../test/OutfitTransferLifecycle.t.sol) for custody and return behavior.
|
|
26
26
|
- [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) for adversarial flows.
|
|
27
|
-
- [`test/Fork.t.sol`](../test/Fork.t.sol), [`test/
|
|
27
|
+
- [`test/Fork.t.sol`](../test/Fork.t.sol), [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol), and [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) for integration and pinned edge cases.
|
package/script/Add.Denver.s.sol
CHANGED
|
@@ -80,6 +80,10 @@ contract Drop1Script is Script, Sphinx {
|
|
|
80
80
|
splits: new JBSplit[](0)
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
// Capture pre-existing maxTierIdOf so we can detect drift between Sphinx proposal-time simulation and
|
|
84
|
+
// execution (see Drop1.s.sol for the same pattern).
|
|
85
|
+
uint256 maxTierIdBeforeAdjust = hook.STORE().maxTierIdOf(address(hook));
|
|
86
|
+
|
|
83
87
|
hook.adjustTiers({tiersToAdd: products, tierIdsToRemove: new uint256[](0)});
|
|
84
88
|
|
|
85
89
|
// Read maxTierIdOf after adjustTiers so the value reflects our newly added tiers,
|
|
@@ -87,6 +91,9 @@ contract Drop1Script is Script, Sphinx {
|
|
|
87
91
|
// the read and the adjustTiers call.
|
|
88
92
|
uint256 maxTierId = hook.STORE().maxTierIdOf(address(hook));
|
|
89
93
|
|
|
94
|
+
// Drift detection: our single tier should occupy exactly maxTierIdBeforeAdjust + 1.
|
|
95
|
+
require(maxTierId == maxTierIdBeforeAdjust + 1, "Add.Denver: maxTierIdOf drift between proposal and execution");
|
|
96
|
+
|
|
90
97
|
// Build the product IDs array for the newly added tier(s).
|
|
91
98
|
uint256[] memory productIds = new uint256[](1);
|
|
92
99
|
productIds[0] = maxTierId;
|
package/script/Deploy.s.sol
CHANGED
|
@@ -391,10 +391,10 @@ contract DeployScript is Script, Sphinx {
|
|
|
391
391
|
'<g class="o"><path d="M190 127h3v3h-3zm3 13h4v3h-4zm-42 0h6v6h-6z"/><path d="M151 133h3v7h-3zm10 0h6v4h-6z"/><path d="M157 137h17v6h-17zm3 13h14v3h-14zm17-13h7v16h-7z"/><path d="M184 137h6v6h-6zm0 10h10v6h-10z"/><path d="M187 143h10v4h-10z"/><path d="M190 140h3v3h-3zm-6-10h3v7h-3z"/><path d="M187 130h6v3h-6zm-36 0h10v3h-10zm16 13h7v7h-7zm-10 0h7v7h-7z"/><path d="M164 147h3v3h-3zm29-20h4v6h-4z"/><path d="M194 133h3v7h-3z"/></g><g class="w"><path d="M154 133h7v4h-7z"/><path d="M154 137h3v3h-3zm10 6h3v4h-3zm20 0h3v4h-3zm3-10h7v4h-7z"/><path d="M190 137h4v3h-4z"/></g>';
|
|
392
392
|
{
|
|
393
393
|
// Perform the check for the resolver..
|
|
394
|
-
(address _resolver, bool _resolverIsDeployed) = _isDeployed(
|
|
395
|
-
RESOLVER_SALT,
|
|
396
|
-
type(Banny721TokenUriResolver).creationCode,
|
|
397
|
-
abi.encode(
|
|
394
|
+
(address _resolver, bool _resolverIsDeployed) = _isDeployed({
|
|
395
|
+
salt: RESOLVER_SALT,
|
|
396
|
+
creationCode: type(Banny721TokenUriResolver).creationCode,
|
|
397
|
+
arguments: abi.encode(
|
|
398
398
|
bannyBodySvg,
|
|
399
399
|
defaultNecklaceSvg,
|
|
400
400
|
defaultMouthSvg,
|
|
@@ -403,7 +403,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
403
403
|
operator,
|
|
404
404
|
trustedForwarder
|
|
405
405
|
)
|
|
406
|
-
);
|
|
406
|
+
});
|
|
407
407
|
// Deploy it if it has not been deployed yet.
|
|
408
408
|
resolver = !_resolverIsDeployed
|
|
409
409
|
? new Banny721TokenUriResolver{salt: RESOLVER_SALT}({
|
package/script/Drop1.s.sol
CHANGED
|
@@ -1185,6 +1185,12 @@ contract Drop1Script is Script, Sphinx {
|
|
|
1185
1185
|
splits: new JBSplit[](0)
|
|
1186
1186
|
});
|
|
1187
1187
|
|
|
1188
|
+
// Capture the pre-existing maxTierIdOf so we can detect drift between Sphinx proposal-time simulation and
|
|
1189
|
+
// execution. Without this guard, an authorized `ADJUST_721_TIERS` call landing between proposal and
|
|
1190
|
+
// execution would shift our 47 new tier IDs upward, and the metadata writes below would silently target
|
|
1191
|
+
// the wrong UPC range (or land on tiers that did not get our SVG/name data).
|
|
1192
|
+
uint256 maxTierIdBeforeAdjust = hook.STORE().maxTierIdOf(address(hook));
|
|
1193
|
+
|
|
1188
1194
|
hook.adjustTiers({tiersToAdd: products, tierIdsToRemove: new uint256[](0)});
|
|
1189
1195
|
|
|
1190
1196
|
// Read maxTierIdOf after adjustTiers so the value reflects our newly added tiers,
|
|
@@ -1192,6 +1198,11 @@ contract Drop1Script is Script, Sphinx {
|
|
|
1192
1198
|
// the read and the adjustTiers call.
|
|
1193
1199
|
uint256 maxTierId = hook.STORE().maxTierIdOf(address(hook));
|
|
1194
1200
|
|
|
1201
|
+
// Drift detection: our 47 tiers should occupy exactly the range (maxTierIdBeforeAdjust, maxTierId]. If
|
|
1202
|
+
// another transaction added tiers between proposal and execution, the range no longer matches the 47-tier
|
|
1203
|
+
// assumption and the metadata writes would target the wrong UPCs.
|
|
1204
|
+
require(maxTierId == maxTierIdBeforeAdjust + 47, "Drop1: maxTierIdOf drift between proposal and execution");
|
|
1205
|
+
|
|
1195
1206
|
// Build the product IDs array for the newly added tiers.
|
|
1196
1207
|
// The last 47 tier IDs correspond to the 47 tiers we just added.
|
|
1197
1208
|
uint256[] memory productIds = new uint256[](47);
|
|
@@ -35,25 +35,31 @@ contract Banny721TokenUriResolver is
|
|
|
35
35
|
// --------------------------- custom errors ------------------------- //
|
|
36
36
|
//*********************************************************************//
|
|
37
37
|
|
|
38
|
-
error Banny721TokenUriResolver_ArrayLengthMismatch();
|
|
39
|
-
error Banny721TokenUriResolver_BannyBodyNotBodyCategory();
|
|
40
|
-
error Banny721TokenUriResolver_CantAccelerateTheLock(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
error
|
|
44
|
-
error
|
|
45
|
-
error
|
|
46
|
-
error
|
|
47
|
-
error
|
|
48
|
-
error
|
|
49
|
-
error
|
|
50
|
-
error
|
|
51
|
-
error
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
error
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
error Banny721TokenUriResolver_ArrayLengthMismatch(uint256 firstLength, uint256 secondLength);
|
|
39
|
+
error Banny721TokenUriResolver_BannyBodyNotBodyCategory(address hook, uint256 bannyBodyId, uint256 category);
|
|
40
|
+
error Banny721TokenUriResolver_CantAccelerateTheLock(
|
|
41
|
+
address hook, uint256 bannyBodyId, uint256 currentLockedUntil, uint256 newLockUntil
|
|
42
|
+
);
|
|
43
|
+
error Banny721TokenUriResolver_ContentsAlreadyStored(uint256 upc);
|
|
44
|
+
error Banny721TokenUriResolver_ContentsMismatch(uint256 upc, bytes32 expectedHash, bytes32 actualHash);
|
|
45
|
+
error Banny721TokenUriResolver_DuplicateCategory(uint256 category);
|
|
46
|
+
error Banny721TokenUriResolver_HashAlreadyStored(uint256 upc, bytes32 existingHash);
|
|
47
|
+
error Banny721TokenUriResolver_HashNotFound(uint256 upc);
|
|
48
|
+
error Banny721TokenUriResolver_HeadAlreadyAdded(uint256 category);
|
|
49
|
+
error Banny721TokenUriResolver_OutfitChangesLocked(address hook, uint256 bannyBodyId, uint256 lockedUntil);
|
|
50
|
+
error Banny721TokenUriResolver_SuitAlreadyAdded(uint256 category);
|
|
51
|
+
error Banny721TokenUriResolver_UnauthorizedBackground(
|
|
52
|
+
address hook, uint256 backgroundId, address sender, address owner
|
|
53
|
+
);
|
|
54
|
+
error Banny721TokenUriResolver_UnauthorizedBannyBody(
|
|
55
|
+
address hook, uint256 bannyBodyId, address sender, address owner
|
|
56
|
+
);
|
|
57
|
+
error Banny721TokenUriResolver_UnauthorizedOutfit(address hook, uint256 outfitId, address sender, address owner);
|
|
58
|
+
error Banny721TokenUriResolver_UnauthorizedTransfer(address operator, address expectedOperator);
|
|
59
|
+
error Banny721TokenUriResolver_UnorderedCategories(uint256 previousCategory, uint256 nextCategory);
|
|
60
|
+
error Banny721TokenUriResolver_UnrecognizedBackground(address hook, uint256 backgroundId, uint256 category);
|
|
61
|
+
error Banny721TokenUriResolver_UnrecognizedCategory(uint256 category);
|
|
62
|
+
error Banny721TokenUriResolver_UnrecognizedProduct(uint256 upc);
|
|
57
63
|
|
|
58
64
|
//*********************************************************************//
|
|
59
65
|
// ------------------------ private constants ------------------------ //
|
|
@@ -112,10 +118,19 @@ contract Banny721TokenUriResolver is
|
|
|
112
118
|
/// @custom:param upc The universal product code that the SVG hash represent.
|
|
113
119
|
mapping(uint256 upc => bytes32) public override svgHashOf;
|
|
114
120
|
|
|
121
|
+
/// @notice The default alien-eye SVG fragment used when rendering a Banny without custom alien eyes.
|
|
115
122
|
string public override DEFAULT_ALIEN_EYES;
|
|
123
|
+
|
|
124
|
+
/// @notice The default mouth SVG fragment used when rendering a Banny without a custom mouth.
|
|
116
125
|
string public override DEFAULT_MOUTH;
|
|
126
|
+
|
|
127
|
+
/// @notice The default necklace SVG fragment used when rendering a Banny without a custom necklace.
|
|
117
128
|
string public override DEFAULT_NECKLACE;
|
|
129
|
+
|
|
130
|
+
/// @notice The default standard-eye SVG fragment used when rendering a Banny without custom standard eyes.
|
|
118
131
|
string public override DEFAULT_STANDARD_EYES;
|
|
132
|
+
|
|
133
|
+
/// @notice The base Banny body SVG fragment used as the starting layer for token rendering.
|
|
119
134
|
string public override BANNY_BODY;
|
|
120
135
|
|
|
121
136
|
//*********************************************************************//
|
|
@@ -317,7 +332,6 @@ contract Banny721TokenUriResolver is
|
|
|
317
332
|
}
|
|
318
333
|
|
|
319
334
|
// Get a reference to the pricing context.
|
|
320
|
-
// slither-disable-next-line unused-return
|
|
321
335
|
(uint256 currency, uint256 decimals) = IJB721TiersHook(hook).pricingContext();
|
|
322
336
|
|
|
323
337
|
attributes = string.concat(
|
|
@@ -627,7 +641,12 @@ contract Banny721TokenUriResolver is
|
|
|
627
641
|
/// @param hook The 721 contract of the token having ownership checked.
|
|
628
642
|
/// @param upc The product's UPC to check ownership of.
|
|
629
643
|
function _checkIfSenderIsOwner(address hook, uint256 upc) internal view {
|
|
630
|
-
|
|
644
|
+
address owner = IERC721(hook).ownerOf(upc);
|
|
645
|
+
if (owner != _msgSender()) {
|
|
646
|
+
revert Banny721TokenUriResolver_UnauthorizedBannyBody({
|
|
647
|
+
hook: hook, bannyBodyId: upc, sender: _msgSender(), owner: owner
|
|
648
|
+
});
|
|
649
|
+
}
|
|
631
650
|
}
|
|
632
651
|
|
|
633
652
|
/// @notice The length of the context suffix appended by a trusted forwarder.
|
|
@@ -656,7 +675,6 @@ contract Banny721TokenUriResolver is
|
|
|
656
675
|
view
|
|
657
676
|
returns (string memory)
|
|
658
677
|
{
|
|
659
|
-
// slither-disable-next-line encode-packed-collision
|
|
660
678
|
return string.concat(
|
|
661
679
|
"data:application/json;base64,",
|
|
662
680
|
Base64.encode(
|
|
@@ -716,7 +734,7 @@ contract Banny721TokenUriResolver is
|
|
|
716
734
|
return ("ffe900", "ffc700", "f3a603", "965a1a", "ffe900", "ffc700", "f3a603");
|
|
717
735
|
}
|
|
718
736
|
|
|
719
|
-
revert Banny721TokenUriResolver_UnrecognizedProduct();
|
|
737
|
+
revert Banny721TokenUriResolver_UnrecognizedProduct({upc: upc});
|
|
720
738
|
}
|
|
721
739
|
|
|
722
740
|
/// @notice The full name of each product, including category and inventory.
|
|
@@ -953,7 +971,9 @@ contract Banny721TokenUriResolver is
|
|
|
953
971
|
// Outfit locks are user-selected display locks; timestamp tolerance is acceptable here.
|
|
954
972
|
// forge-lint: disable-next-line(block-timestamp)
|
|
955
973
|
if (bannyBodyId != 0 && bannyBodyId != exemptBodyId && outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
|
|
956
|
-
revert Banny721TokenUriResolver_OutfitChangesLocked(
|
|
974
|
+
revert Banny721TokenUriResolver_OutfitChangesLocked({
|
|
975
|
+
hook: hook, bannyBodyId: bannyBodyId, lockedUntil: outfitLockedUntil[hook][bannyBodyId]
|
|
976
|
+
});
|
|
957
977
|
}
|
|
958
978
|
}
|
|
959
979
|
|
|
@@ -1067,9 +1087,9 @@ contract Banny721TokenUriResolver is
|
|
|
1067
1087
|
}
|
|
1068
1088
|
|
|
1069
1089
|
// A full HEAD and individual head accessories cannot coexist — the head would hide the accessories.
|
|
1070
|
-
if (hasHead && hasHeadAccessory) revert Banny721TokenUriResolver_HeadAlreadyAdded();
|
|
1090
|
+
if (hasHead && hasHeadAccessory) revert Banny721TokenUriResolver_HeadAlreadyAdded({category: _HEAD_CATEGORY});
|
|
1071
1091
|
// A full SUIT and individual suit pieces cannot coexist — the suit would hide the pieces.
|
|
1072
|
-
if (hasSuit && hasSuitPiece) revert Banny721TokenUriResolver_SuitAlreadyAdded();
|
|
1092
|
+
if (hasSuit && hasSuitPiece) revert Banny721TokenUriResolver_SuitAlreadyAdded({category: _SUIT_CATEGORY});
|
|
1073
1093
|
}
|
|
1074
1094
|
|
|
1075
1095
|
//*********************************************************************//
|
|
@@ -1117,18 +1137,28 @@ contract Banny721TokenUriResolver is
|
|
|
1117
1137
|
// Cache the sender once to avoid repeated ERC-2771 context reads throughout the call chain.
|
|
1118
1138
|
address sender = _msgSender();
|
|
1119
1139
|
|
|
1120
|
-
|
|
1140
|
+
address bannyBodyOwner = IERC721(hook).ownerOf(bannyBodyId);
|
|
1141
|
+
if (bannyBodyOwner != sender) {
|
|
1142
|
+
revert Banny721TokenUriResolver_UnauthorizedBannyBody({
|
|
1143
|
+
hook: hook, bannyBodyId: bannyBodyId, sender: sender, owner: bannyBodyOwner
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1121
1146
|
|
|
1122
1147
|
// Make sure the bannyBodyId belongs to a body-category tier.
|
|
1123
|
-
|
|
1124
|
-
|
|
1148
|
+
uint256 bannyBodyCategory = _productOfTokenId({hook: hook, tokenId: bannyBodyId}).category;
|
|
1149
|
+
if (bannyBodyCategory != _BODY_CATEGORY) {
|
|
1150
|
+
revert Banny721TokenUriResolver_BannyBodyNotBodyCategory({
|
|
1151
|
+
hook: hook, bannyBodyId: bannyBodyId, category: bannyBodyCategory
|
|
1152
|
+
});
|
|
1125
1153
|
}
|
|
1126
1154
|
|
|
1127
1155
|
// Can't decorate a banny that's locked.
|
|
1128
1156
|
// Outfit locks are user-selected display locks; timestamp tolerance is acceptable here.
|
|
1129
1157
|
// forge-lint: disable-next-line(block-timestamp)
|
|
1130
1158
|
if (outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
|
|
1131
|
-
revert Banny721TokenUriResolver_OutfitChangesLocked(
|
|
1159
|
+
revert Banny721TokenUriResolver_OutfitChangesLocked({
|
|
1160
|
+
hook: hook, bannyBodyId: bannyBodyId, lockedUntil: outfitLockedUntil[hook][bannyBodyId]
|
|
1161
|
+
});
|
|
1132
1162
|
}
|
|
1133
1163
|
|
|
1134
1164
|
emit DecorateBanny({
|
|
@@ -1156,7 +1186,11 @@ contract Banny721TokenUriResolver is
|
|
|
1156
1186
|
uint256 newLockUntil = block.timestamp + _LOCK_DURATION;
|
|
1157
1187
|
|
|
1158
1188
|
// Make sure the new lock is at least as big as the current lock.
|
|
1159
|
-
if (currentLockedUntil > newLockUntil)
|
|
1189
|
+
if (currentLockedUntil > newLockUntil) {
|
|
1190
|
+
revert Banny721TokenUriResolver_CantAccelerateTheLock({
|
|
1191
|
+
hook: hook, bannyBodyId: bannyBodyId, currentLockedUntil: currentLockedUntil, newLockUntil: newLockUntil
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1160
1194
|
|
|
1161
1195
|
// Set the lock.
|
|
1162
1196
|
outfitLockedUntil[hook][bannyBodyId] = newLockUntil;
|
|
@@ -1188,7 +1222,9 @@ contract Banny721TokenUriResolver is
|
|
|
1188
1222
|
data; // unused.
|
|
1189
1223
|
|
|
1190
1224
|
// Make sure the transaction's operator is this contract.
|
|
1191
|
-
if (operator != address(this))
|
|
1225
|
+
if (operator != address(this)) {
|
|
1226
|
+
revert Banny721TokenUriResolver_UnauthorizedTransfer({operator: operator, expectedOperator: address(this)});
|
|
1227
|
+
}
|
|
1192
1228
|
|
|
1193
1229
|
return IERC721Receiver.onERC721Received.selector;
|
|
1194
1230
|
}
|
|
@@ -1221,7 +1257,9 @@ contract Banny721TokenUriResolver is
|
|
|
1221
1257
|
/// @param upcs The universal product codes of the products having their name stored.
|
|
1222
1258
|
/// @param names The names of the products.
|
|
1223
1259
|
function setProductNames(uint256[] memory upcs, string[] memory names) external override onlyOwner {
|
|
1224
|
-
if (upcs.length != names.length)
|
|
1260
|
+
if (upcs.length != names.length) {
|
|
1261
|
+
revert Banny721TokenUriResolver_ArrayLengthMismatch({firstLength: upcs.length, secondLength: names.length});
|
|
1262
|
+
}
|
|
1225
1263
|
|
|
1226
1264
|
address sender = _msgSender();
|
|
1227
1265
|
for (uint256 i; i < upcs.length;) {
|
|
@@ -1237,11 +1275,18 @@ contract Banny721TokenUriResolver is
|
|
|
1237
1275
|
}
|
|
1238
1276
|
}
|
|
1239
1277
|
|
|
1240
|
-
/// @notice
|
|
1278
|
+
/// @notice Permissionless: anyone can store the SVG bytes for a product code, as long as they match the owner-
|
|
1279
|
+
/// precommitted hash. The hash is set by the owner via `setSvgHashesOf`; this function only finalizes the bytes.
|
|
1280
|
+
/// @dev See `setSvgHashesOf` for the trust model. The function reverts on hash mismatch, missing hash, or
|
|
1281
|
+
/// already-stored content. Publication is one-shot — wrong-hash commitments are unrecoverable by design.
|
|
1241
1282
|
/// @param upcs The universal product codes of the products having SVGs stored.
|
|
1242
1283
|
/// @param svgContents The svg contents to store, not including the parent <svg></svg> element.
|
|
1243
1284
|
function setSvgContentsOf(uint256[] memory upcs, string[] calldata svgContents) external override {
|
|
1244
|
-
if (upcs.length != svgContents.length)
|
|
1285
|
+
if (upcs.length != svgContents.length) {
|
|
1286
|
+
revert Banny721TokenUriResolver_ArrayLengthMismatch({
|
|
1287
|
+
firstLength: upcs.length, secondLength: svgContents.length
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1245
1290
|
|
|
1246
1291
|
address sender = _msgSender();
|
|
1247
1292
|
for (uint256 i; i < upcs.length;) {
|
|
@@ -1249,16 +1294,23 @@ contract Banny721TokenUriResolver is
|
|
|
1249
1294
|
string memory svgContent = svgContents[i];
|
|
1250
1295
|
|
|
1251
1296
|
// Make sure there isn't already contents for the specified universal product code.
|
|
1252
|
-
if (bytes(_svgContentOf[upc]).length != 0)
|
|
1297
|
+
if (bytes(_svgContentOf[upc]).length != 0) {
|
|
1298
|
+
revert Banny721TokenUriResolver_ContentsAlreadyStored({upc: upc});
|
|
1299
|
+
}
|
|
1253
1300
|
|
|
1254
1301
|
// Get the stored svg hash for the product.
|
|
1255
1302
|
bytes32 svgHash = svgHashOf[upc];
|
|
1256
1303
|
|
|
1257
1304
|
// Make sure a hash exists.
|
|
1258
|
-
if (svgHash == bytes32(0)) revert Banny721TokenUriResolver_HashNotFound();
|
|
1305
|
+
if (svgHash == bytes32(0)) revert Banny721TokenUriResolver_HashNotFound({upc: upc});
|
|
1259
1306
|
|
|
1260
1307
|
// Make sure the content matches the hash.
|
|
1261
|
-
|
|
1308
|
+
bytes32 actualHash = keccak256(abi.encodePacked(svgContent));
|
|
1309
|
+
if (actualHash != svgHash) {
|
|
1310
|
+
revert Banny721TokenUriResolver_ContentsMismatch({
|
|
1311
|
+
upc: upc, expectedHash: svgHash, actualHash: actualHash
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1262
1314
|
|
|
1263
1315
|
// Store the svg contents.
|
|
1264
1316
|
_svgContentOf[upc] = svgContent;
|
|
@@ -1275,7 +1327,11 @@ contract Banny721TokenUriResolver is
|
|
|
1275
1327
|
/// @param upcs The universal product codes of the products having SVG hashes stored.
|
|
1276
1328
|
/// @param svgHashes The svg hashes to store, not including the parent <svg></svg> element.
|
|
1277
1329
|
function setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes) external override onlyOwner {
|
|
1278
|
-
if (upcs.length != svgHashes.length)
|
|
1330
|
+
if (upcs.length != svgHashes.length) {
|
|
1331
|
+
revert Banny721TokenUriResolver_ArrayLengthMismatch({
|
|
1332
|
+
firstLength: upcs.length, secondLength: svgHashes.length
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1279
1335
|
|
|
1280
1336
|
address sender = _msgSender();
|
|
1281
1337
|
for (uint256 i; i < upcs.length;) {
|
|
@@ -1283,7 +1339,9 @@ contract Banny721TokenUriResolver is
|
|
|
1283
1339
|
bytes32 svgHash = svgHashes[i];
|
|
1284
1340
|
|
|
1285
1341
|
// Make sure there isn't already contents for the specified universal product code.
|
|
1286
|
-
if (svgHashOf[upc] != bytes32(0))
|
|
1342
|
+
if (svgHashOf[upc] != bytes32(0)) {
|
|
1343
|
+
revert Banny721TokenUriResolver_HashAlreadyStored({upc: upc, existingHash: svgHashOf[upc]});
|
|
1344
|
+
}
|
|
1287
1345
|
|
|
1288
1346
|
// Store the svg contents.
|
|
1289
1347
|
svgHashOf[upc] = svgHash;
|
|
@@ -1331,11 +1389,18 @@ contract Banny721TokenUriResolver is
|
|
|
1331
1389
|
uint256 userId = userOf({hook: hook, backgroundId: backgroundId});
|
|
1332
1390
|
|
|
1333
1391
|
// If the background is not currently used, only the background's owner can use it for decoration.
|
|
1334
|
-
if (userId == 0)
|
|
1392
|
+
if (userId == 0) {
|
|
1393
|
+
revert Banny721TokenUriResolver_UnauthorizedBackground({
|
|
1394
|
+
hook: hook, backgroundId: backgroundId, sender: sender, owner: owner
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1335
1397
|
|
|
1336
1398
|
// If the background is used, the banny body's owner can also authorize its use.
|
|
1337
|
-
|
|
1338
|
-
|
|
1399
|
+
address userOwner = IERC721(hook).ownerOf(userId);
|
|
1400
|
+
if (sender != userOwner) {
|
|
1401
|
+
revert Banny721TokenUriResolver_UnauthorizedBackground({
|
|
1402
|
+
hook: hook, backgroundId: backgroundId, sender: sender, owner: userOwner
|
|
1403
|
+
});
|
|
1339
1404
|
}
|
|
1340
1405
|
|
|
1341
1406
|
// A locked source body keeps its equipped background until the lock expires.
|
|
@@ -1347,7 +1412,9 @@ contract Banny721TokenUriResolver is
|
|
|
1347
1412
|
|
|
1348
1413
|
// Background must exist and must be a background category.
|
|
1349
1414
|
if (backgroundProduct.id == 0 || backgroundProduct.category != _BACKGROUND_CATEGORY) {
|
|
1350
|
-
revert Banny721TokenUriResolver_UnrecognizedBackground(
|
|
1415
|
+
revert Banny721TokenUriResolver_UnrecognizedBackground({
|
|
1416
|
+
hook: hook, backgroundId: backgroundId, category: backgroundProduct.category
|
|
1417
|
+
});
|
|
1351
1418
|
}
|
|
1352
1419
|
|
|
1353
1420
|
// Try to transfer the previous background back before updating state.
|
|
@@ -1434,18 +1501,23 @@ contract Banny721TokenUriResolver is
|
|
|
1434
1501
|
|
|
1435
1502
|
// Check if the call is being made either by the outfit's owner or the owner of the banny body currently
|
|
1436
1503
|
// wearing it.
|
|
1437
|
-
// slither-disable-next-line calls-loop
|
|
1438
1504
|
if (sender != IERC721(hook).ownerOf(outfitId)) {
|
|
1439
1505
|
// Get the banny body currently wearing this outfit.
|
|
1440
1506
|
uint256 wearerId = wearerOf({hook: hook, outfitId: outfitId});
|
|
1441
1507
|
|
|
1442
1508
|
// If the outfit is not currently worn, only the outfit's owner can use it for decoration.
|
|
1443
|
-
if (wearerId == 0)
|
|
1509
|
+
if (wearerId == 0) {
|
|
1510
|
+
revert Banny721TokenUriResolver_UnauthorizedOutfit({
|
|
1511
|
+
hook: hook, outfitId: outfitId, sender: sender, owner: IERC721(hook).ownerOf(outfitId)
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1444
1514
|
|
|
1445
1515
|
// If the outfit is worn, the banny body's owner can also authorize its use.
|
|
1446
|
-
|
|
1447
|
-
if (sender !=
|
|
1448
|
-
revert Banny721TokenUriResolver_UnauthorizedOutfit(
|
|
1516
|
+
address wearerOwner = IERC721(hook).ownerOf(wearerId);
|
|
1517
|
+
if (sender != wearerOwner) {
|
|
1518
|
+
revert Banny721TokenUriResolver_UnauthorizedOutfit({
|
|
1519
|
+
hook: hook, outfitId: outfitId, sender: sender, owner: wearerOwner
|
|
1520
|
+
});
|
|
1449
1521
|
}
|
|
1450
1522
|
|
|
1451
1523
|
// A locked source body keeps its equipped outfits until the lock expires.
|
|
@@ -1457,12 +1529,14 @@ contract Banny721TokenUriResolver is
|
|
|
1457
1529
|
|
|
1458
1530
|
// The product's category must be a known category.
|
|
1459
1531
|
if (outfitProductCategory < _BACKSIDE_CATEGORY || outfitProductCategory > _SPECIAL_BODY_CATEGORY) {
|
|
1460
|
-
revert Banny721TokenUriResolver_UnrecognizedCategory();
|
|
1532
|
+
revert Banny721TokenUriResolver_UnrecognizedCategory({category: outfitProductCategory});
|
|
1461
1533
|
}
|
|
1462
1534
|
|
|
1463
1535
|
// Make sure the category is an increment of the previous outfit's category.
|
|
1464
1536
|
if (i != 0 && outfitProductCategory <= lastAssetCategory) {
|
|
1465
|
-
revert Banny721TokenUriResolver_UnorderedCategories(
|
|
1537
|
+
revert Banny721TokenUriResolver_UnorderedCategories({
|
|
1538
|
+
previousCategory: lastAssetCategory, nextCategory: outfitProductCategory
|
|
1539
|
+
});
|
|
1466
1540
|
}
|
|
1467
1541
|
|
|
1468
1542
|
if (outfitProductCategory == _HEAD_CATEGORY) {
|
|
@@ -1475,12 +1549,12 @@ contract Banny721TokenUriResolver is
|
|
|
1475
1549
|
|| outfitProductCategory == _MOUTH_CATEGORY
|
|
1476
1550
|
|| outfitProductCategory == _HEADTOP_CATEGORY) && hasHead
|
|
1477
1551
|
) {
|
|
1478
|
-
revert Banny721TokenUriResolver_HeadAlreadyAdded();
|
|
1552
|
+
revert Banny721TokenUriResolver_HeadAlreadyAdded({category: outfitProductCategory});
|
|
1479
1553
|
} else if (
|
|
1480
1554
|
(outfitProductCategory == _SUIT_TOP_CATEGORY || outfitProductCategory == _SUIT_BOTTOM_CATEGORY)
|
|
1481
1555
|
&& hasSuit
|
|
1482
1556
|
) {
|
|
1483
|
-
revert Banny721TokenUriResolver_SuitAlreadyAdded();
|
|
1557
|
+
revert Banny721TokenUriResolver_SuitAlreadyAdded({category: outfitProductCategory});
|
|
1484
1558
|
}
|
|
1485
1559
|
|
|
1486
1560
|
// Remove all previous assets up to and including the current category being iterated on.
|
|
@@ -1523,7 +1597,6 @@ contract Banny721TokenUriResolver is
|
|
|
1523
1597
|
_wearerOf[hook][outfitId] = bannyBodyId;
|
|
1524
1598
|
|
|
1525
1599
|
// Transfer the outfit to this contract.
|
|
1526
|
-
// slither-disable-next-line reentrancy-no-eth,calls-loop
|
|
1527
1600
|
if (IERC721(hook).ownerOf(outfitId) != address(this)) {
|
|
1528
1601
|
_transferFrom({hook: hook, from: sender, to: address(this), assetId: outfitId});
|
|
1529
1602
|
}
|
|
@@ -1632,7 +1705,9 @@ contract Banny721TokenUriResolver is
|
|
|
1632
1705
|
_productOfTokenId({hook: hook, tokenId: mergedOutfitIds[i]}).category
|
|
1633
1706
|
== _productOfTokenId({hook: hook, tokenId: mergedOutfitIds[i - 1]}).category
|
|
1634
1707
|
) {
|
|
1635
|
-
revert Banny721TokenUriResolver_DuplicateCategory(
|
|
1708
|
+
revert Banny721TokenUriResolver_DuplicateCategory({
|
|
1709
|
+
category: _productOfTokenId({hook: hook, tokenId: mergedOutfitIds[i]}).category
|
|
1710
|
+
});
|
|
1636
1711
|
}
|
|
1637
1712
|
unchecked {
|
|
1638
1713
|
++i;
|
|
@@ -1661,7 +1736,6 @@ contract Banny721TokenUriResolver is
|
|
|
1661
1736
|
/// @param assetId The ID of the token to transfer.
|
|
1662
1737
|
/// @return success Whether the transfer succeeded.
|
|
1663
1738
|
function _tryTransferFrom(address hook, address from, address to, uint256 assetId) internal returns (bool success) {
|
|
1664
|
-
// slither-disable-next-line reentrancy-no-eth
|
|
1665
1739
|
try IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId}) {
|
|
1666
1740
|
success = true;
|
|
1667
1741
|
} catch {}
|