@bannynet/core-v6 0.0.27 → 0.0.29
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 +1 -1
- 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 +9 -8
- package/script/Drop1.s.sol +11 -0
- package/src/Banny721TokenUriResolver.sol +126 -55
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
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
|
@@ -319,13 +319,13 @@ contract DeployScript is Script, Sphinx {
|
|
|
319
319
|
suckerDeployerConfigurations = new JBSuckerDeployerConfig[](3);
|
|
320
320
|
// OP
|
|
321
321
|
suckerDeployerConfigurations[0] =
|
|
322
|
-
JBSuckerDeployerConfig({deployer: suckers.optimismDeployer, mappings: tokenMappings});
|
|
322
|
+
JBSuckerDeployerConfig({deployer: suckers.optimismDeployer, peer: bytes32(0), mappings: tokenMappings});
|
|
323
323
|
|
|
324
324
|
suckerDeployerConfigurations[1] =
|
|
325
|
-
JBSuckerDeployerConfig({deployer: suckers.baseDeployer, mappings: tokenMappings});
|
|
325
|
+
JBSuckerDeployerConfig({deployer: suckers.baseDeployer, peer: bytes32(0), mappings: tokenMappings});
|
|
326
326
|
|
|
327
327
|
suckerDeployerConfigurations[2] =
|
|
328
|
-
JBSuckerDeployerConfig({deployer: suckers.arbitrumDeployer, mappings: tokenMappings});
|
|
328
|
+
JBSuckerDeployerConfig({deployer: suckers.arbitrumDeployer, peer: bytes32(0), mappings: tokenMappings});
|
|
329
329
|
} else {
|
|
330
330
|
suckerDeployerConfigurations = new JBSuckerDeployerConfig[](1);
|
|
331
331
|
// L2 -> Mainnet
|
|
@@ -333,6 +333,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
333
333
|
deployer: address(suckers.optimismDeployer) != address(0)
|
|
334
334
|
? suckers.optimismDeployer
|
|
335
335
|
: address(suckers.baseDeployer) != address(0) ? suckers.baseDeployer : suckers.arbitrumDeployer,
|
|
336
|
+
peer: bytes32(0),
|
|
336
337
|
mappings: tokenMappings
|
|
337
338
|
});
|
|
338
339
|
|
|
@@ -390,10 +391,10 @@ contract DeployScript is Script, Sphinx {
|
|
|
390
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>';
|
|
391
392
|
{
|
|
392
393
|
// Perform the check for the resolver..
|
|
393
|
-
(address _resolver, bool _resolverIsDeployed) = _isDeployed(
|
|
394
|
-
RESOLVER_SALT,
|
|
395
|
-
type(Banny721TokenUriResolver).creationCode,
|
|
396
|
-
abi.encode(
|
|
394
|
+
(address _resolver, bool _resolverIsDeployed) = _isDeployed({
|
|
395
|
+
salt: RESOLVER_SALT,
|
|
396
|
+
creationCode: type(Banny721TokenUriResolver).creationCode,
|
|
397
|
+
arguments: abi.encode(
|
|
397
398
|
bannyBodySvg,
|
|
398
399
|
defaultNecklaceSvg,
|
|
399
400
|
defaultMouthSvg,
|
|
@@ -402,7 +403,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
402
403
|
operator,
|
|
403
404
|
trustedForwarder
|
|
404
405
|
)
|
|
405
|
-
);
|
|
406
|
+
});
|
|
406
407
|
// Deploy it if it has not been deployed yet.
|
|
407
408
|
resolver = !_resolverIsDeployed
|
|
408
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;) {
|
|
@@ -1241,7 +1279,11 @@ contract Banny721TokenUriResolver is
|
|
|
1241
1279
|
/// @param upcs The universal product codes of the products having SVGs stored.
|
|
1242
1280
|
/// @param svgContents The svg contents to store, not including the parent <svg></svg> element.
|
|
1243
1281
|
function setSvgContentsOf(uint256[] memory upcs, string[] calldata svgContents) external override {
|
|
1244
|
-
if (upcs.length != svgContents.length)
|
|
1282
|
+
if (upcs.length != svgContents.length) {
|
|
1283
|
+
revert Banny721TokenUriResolver_ArrayLengthMismatch({
|
|
1284
|
+
firstLength: upcs.length, secondLength: svgContents.length
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1245
1287
|
|
|
1246
1288
|
address sender = _msgSender();
|
|
1247
1289
|
for (uint256 i; i < upcs.length;) {
|
|
@@ -1249,16 +1291,23 @@ contract Banny721TokenUriResolver is
|
|
|
1249
1291
|
string memory svgContent = svgContents[i];
|
|
1250
1292
|
|
|
1251
1293
|
// Make sure there isn't already contents for the specified universal product code.
|
|
1252
|
-
if (bytes(_svgContentOf[upc]).length != 0)
|
|
1294
|
+
if (bytes(_svgContentOf[upc]).length != 0) {
|
|
1295
|
+
revert Banny721TokenUriResolver_ContentsAlreadyStored({upc: upc});
|
|
1296
|
+
}
|
|
1253
1297
|
|
|
1254
1298
|
// Get the stored svg hash for the product.
|
|
1255
1299
|
bytes32 svgHash = svgHashOf[upc];
|
|
1256
1300
|
|
|
1257
1301
|
// Make sure a hash exists.
|
|
1258
|
-
if (svgHash == bytes32(0)) revert Banny721TokenUriResolver_HashNotFound();
|
|
1302
|
+
if (svgHash == bytes32(0)) revert Banny721TokenUriResolver_HashNotFound({upc: upc});
|
|
1259
1303
|
|
|
1260
1304
|
// Make sure the content matches the hash.
|
|
1261
|
-
|
|
1305
|
+
bytes32 actualHash = keccak256(abi.encodePacked(svgContent));
|
|
1306
|
+
if (actualHash != svgHash) {
|
|
1307
|
+
revert Banny721TokenUriResolver_ContentsMismatch({
|
|
1308
|
+
upc: upc, expectedHash: svgHash, actualHash: actualHash
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1262
1311
|
|
|
1263
1312
|
// Store the svg contents.
|
|
1264
1313
|
_svgContentOf[upc] = svgContent;
|
|
@@ -1275,7 +1324,11 @@ contract Banny721TokenUriResolver is
|
|
|
1275
1324
|
/// @param upcs The universal product codes of the products having SVG hashes stored.
|
|
1276
1325
|
/// @param svgHashes The svg hashes to store, not including the parent <svg></svg> element.
|
|
1277
1326
|
function setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes) external override onlyOwner {
|
|
1278
|
-
if (upcs.length != svgHashes.length)
|
|
1327
|
+
if (upcs.length != svgHashes.length) {
|
|
1328
|
+
revert Banny721TokenUriResolver_ArrayLengthMismatch({
|
|
1329
|
+
firstLength: upcs.length, secondLength: svgHashes.length
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1279
1332
|
|
|
1280
1333
|
address sender = _msgSender();
|
|
1281
1334
|
for (uint256 i; i < upcs.length;) {
|
|
@@ -1283,7 +1336,9 @@ contract Banny721TokenUriResolver is
|
|
|
1283
1336
|
bytes32 svgHash = svgHashes[i];
|
|
1284
1337
|
|
|
1285
1338
|
// Make sure there isn't already contents for the specified universal product code.
|
|
1286
|
-
if (svgHashOf[upc] != bytes32(0))
|
|
1339
|
+
if (svgHashOf[upc] != bytes32(0)) {
|
|
1340
|
+
revert Banny721TokenUriResolver_HashAlreadyStored({upc: upc, existingHash: svgHashOf[upc]});
|
|
1341
|
+
}
|
|
1287
1342
|
|
|
1288
1343
|
// Store the svg contents.
|
|
1289
1344
|
svgHashOf[upc] = svgHash;
|
|
@@ -1331,11 +1386,18 @@ contract Banny721TokenUriResolver is
|
|
|
1331
1386
|
uint256 userId = userOf({hook: hook, backgroundId: backgroundId});
|
|
1332
1387
|
|
|
1333
1388
|
// If the background is not currently used, only the background's owner can use it for decoration.
|
|
1334
|
-
if (userId == 0)
|
|
1389
|
+
if (userId == 0) {
|
|
1390
|
+
revert Banny721TokenUriResolver_UnauthorizedBackground({
|
|
1391
|
+
hook: hook, backgroundId: backgroundId, sender: sender, owner: owner
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1335
1394
|
|
|
1336
1395
|
// If the background is used, the banny body's owner can also authorize its use.
|
|
1337
|
-
|
|
1338
|
-
|
|
1396
|
+
address userOwner = IERC721(hook).ownerOf(userId);
|
|
1397
|
+
if (sender != userOwner) {
|
|
1398
|
+
revert Banny721TokenUriResolver_UnauthorizedBackground({
|
|
1399
|
+
hook: hook, backgroundId: backgroundId, sender: sender, owner: userOwner
|
|
1400
|
+
});
|
|
1339
1401
|
}
|
|
1340
1402
|
|
|
1341
1403
|
// A locked source body keeps its equipped background until the lock expires.
|
|
@@ -1347,7 +1409,9 @@ contract Banny721TokenUriResolver is
|
|
|
1347
1409
|
|
|
1348
1410
|
// Background must exist and must be a background category.
|
|
1349
1411
|
if (backgroundProduct.id == 0 || backgroundProduct.category != _BACKGROUND_CATEGORY) {
|
|
1350
|
-
revert Banny721TokenUriResolver_UnrecognizedBackground(
|
|
1412
|
+
revert Banny721TokenUriResolver_UnrecognizedBackground({
|
|
1413
|
+
hook: hook, backgroundId: backgroundId, category: backgroundProduct.category
|
|
1414
|
+
});
|
|
1351
1415
|
}
|
|
1352
1416
|
|
|
1353
1417
|
// Try to transfer the previous background back before updating state.
|
|
@@ -1434,18 +1498,23 @@ contract Banny721TokenUriResolver is
|
|
|
1434
1498
|
|
|
1435
1499
|
// Check if the call is being made either by the outfit's owner or the owner of the banny body currently
|
|
1436
1500
|
// wearing it.
|
|
1437
|
-
// slither-disable-next-line calls-loop
|
|
1438
1501
|
if (sender != IERC721(hook).ownerOf(outfitId)) {
|
|
1439
1502
|
// Get the banny body currently wearing this outfit.
|
|
1440
1503
|
uint256 wearerId = wearerOf({hook: hook, outfitId: outfitId});
|
|
1441
1504
|
|
|
1442
1505
|
// If the outfit is not currently worn, only the outfit's owner can use it for decoration.
|
|
1443
|
-
if (wearerId == 0)
|
|
1506
|
+
if (wearerId == 0) {
|
|
1507
|
+
revert Banny721TokenUriResolver_UnauthorizedOutfit({
|
|
1508
|
+
hook: hook, outfitId: outfitId, sender: sender, owner: IERC721(hook).ownerOf(outfitId)
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1444
1511
|
|
|
1445
1512
|
// If the outfit is worn, the banny body's owner can also authorize its use.
|
|
1446
|
-
|
|
1447
|
-
if (sender !=
|
|
1448
|
-
revert Banny721TokenUriResolver_UnauthorizedOutfit(
|
|
1513
|
+
address wearerOwner = IERC721(hook).ownerOf(wearerId);
|
|
1514
|
+
if (sender != wearerOwner) {
|
|
1515
|
+
revert Banny721TokenUriResolver_UnauthorizedOutfit({
|
|
1516
|
+
hook: hook, outfitId: outfitId, sender: sender, owner: wearerOwner
|
|
1517
|
+
});
|
|
1449
1518
|
}
|
|
1450
1519
|
|
|
1451
1520
|
// A locked source body keeps its equipped outfits until the lock expires.
|
|
@@ -1457,12 +1526,14 @@ contract Banny721TokenUriResolver is
|
|
|
1457
1526
|
|
|
1458
1527
|
// The product's category must be a known category.
|
|
1459
1528
|
if (outfitProductCategory < _BACKSIDE_CATEGORY || outfitProductCategory > _SPECIAL_BODY_CATEGORY) {
|
|
1460
|
-
revert Banny721TokenUriResolver_UnrecognizedCategory();
|
|
1529
|
+
revert Banny721TokenUriResolver_UnrecognizedCategory({category: outfitProductCategory});
|
|
1461
1530
|
}
|
|
1462
1531
|
|
|
1463
1532
|
// Make sure the category is an increment of the previous outfit's category.
|
|
1464
1533
|
if (i != 0 && outfitProductCategory <= lastAssetCategory) {
|
|
1465
|
-
revert Banny721TokenUriResolver_UnorderedCategories(
|
|
1534
|
+
revert Banny721TokenUriResolver_UnorderedCategories({
|
|
1535
|
+
previousCategory: lastAssetCategory, nextCategory: outfitProductCategory
|
|
1536
|
+
});
|
|
1466
1537
|
}
|
|
1467
1538
|
|
|
1468
1539
|
if (outfitProductCategory == _HEAD_CATEGORY) {
|
|
@@ -1475,12 +1546,12 @@ contract Banny721TokenUriResolver is
|
|
|
1475
1546
|
|| outfitProductCategory == _MOUTH_CATEGORY
|
|
1476
1547
|
|| outfitProductCategory == _HEADTOP_CATEGORY) && hasHead
|
|
1477
1548
|
) {
|
|
1478
|
-
revert Banny721TokenUriResolver_HeadAlreadyAdded();
|
|
1549
|
+
revert Banny721TokenUriResolver_HeadAlreadyAdded({category: outfitProductCategory});
|
|
1479
1550
|
} else if (
|
|
1480
1551
|
(outfitProductCategory == _SUIT_TOP_CATEGORY || outfitProductCategory == _SUIT_BOTTOM_CATEGORY)
|
|
1481
1552
|
&& hasSuit
|
|
1482
1553
|
) {
|
|
1483
|
-
revert Banny721TokenUriResolver_SuitAlreadyAdded();
|
|
1554
|
+
revert Banny721TokenUriResolver_SuitAlreadyAdded({category: outfitProductCategory});
|
|
1484
1555
|
}
|
|
1485
1556
|
|
|
1486
1557
|
// Remove all previous assets up to and including the current category being iterated on.
|
|
@@ -1523,7 +1594,6 @@ contract Banny721TokenUriResolver is
|
|
|
1523
1594
|
_wearerOf[hook][outfitId] = bannyBodyId;
|
|
1524
1595
|
|
|
1525
1596
|
// Transfer the outfit to this contract.
|
|
1526
|
-
// slither-disable-next-line reentrancy-no-eth,calls-loop
|
|
1527
1597
|
if (IERC721(hook).ownerOf(outfitId) != address(this)) {
|
|
1528
1598
|
_transferFrom({hook: hook, from: sender, to: address(this), assetId: outfitId});
|
|
1529
1599
|
}
|
|
@@ -1632,7 +1702,9 @@ contract Banny721TokenUriResolver is
|
|
|
1632
1702
|
_productOfTokenId({hook: hook, tokenId: mergedOutfitIds[i]}).category
|
|
1633
1703
|
== _productOfTokenId({hook: hook, tokenId: mergedOutfitIds[i - 1]}).category
|
|
1634
1704
|
) {
|
|
1635
|
-
revert Banny721TokenUriResolver_DuplicateCategory(
|
|
1705
|
+
revert Banny721TokenUriResolver_DuplicateCategory({
|
|
1706
|
+
category: _productOfTokenId({hook: hook, tokenId: mergedOutfitIds[i]}).category
|
|
1707
|
+
});
|
|
1636
1708
|
}
|
|
1637
1709
|
unchecked {
|
|
1638
1710
|
++i;
|
|
@@ -1661,7 +1733,6 @@ contract Banny721TokenUriResolver is
|
|
|
1661
1733
|
/// @param assetId The ID of the token to transfer.
|
|
1662
1734
|
/// @return success Whether the transfer succeeded.
|
|
1663
1735
|
function _tryTransferFrom(address hook, address from, address to, uint256 assetId) internal returns (bool success) {
|
|
1664
|
-
// slither-disable-next-line reentrancy-no-eth
|
|
1665
1736
|
try IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId}) {
|
|
1666
1737
|
success = true;
|
|
1667
1738
|
} catch {}
|