@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 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/audit/BurnedBodyStrandsAssets.t.sol`
54
- 4. `test/audit/TryTransferFromStrandsAssets.t.sol`
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, audit, QA, and regression coverage
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.28",
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.39",
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": {
@@ -21,5 +21,5 @@
21
21
 
22
22
  ## Useful Proof Points
23
23
 
24
- - [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for security-sensitive assumptions.
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.
@@ -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/TestAuditGaps.sol`](../test/TestAuditGaps.sol), and [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) for integration and pinned edge cases.
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.
@@ -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;
@@ -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}({
@@ -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
- error Banny721TokenUriResolver_ContentsAlreadyStored();
42
- error Banny721TokenUriResolver_ContentsMismatch();
43
- error Banny721TokenUriResolver_DuplicateCategory();
44
- error Banny721TokenUriResolver_HashAlreadyStored();
45
- error Banny721TokenUriResolver_HashNotFound();
46
- error Banny721TokenUriResolver_HeadAlreadyAdded();
47
- error Banny721TokenUriResolver_OutfitChangesLocked();
48
- error Banny721TokenUriResolver_SuitAlreadyAdded();
49
- error Banny721TokenUriResolver_UnauthorizedBackground();
50
- error Banny721TokenUriResolver_UnauthorizedBannyBody();
51
- error Banny721TokenUriResolver_UnauthorizedOutfit();
52
- error Banny721TokenUriResolver_UnauthorizedTransfer();
53
- error Banny721TokenUriResolver_UnorderedCategories();
54
- error Banny721TokenUriResolver_UnrecognizedBackground();
55
- error Banny721TokenUriResolver_UnrecognizedCategory();
56
- error Banny721TokenUriResolver_UnrecognizedProduct();
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
- if (IERC721(hook).ownerOf(upc) != _msgSender()) revert Banny721TokenUriResolver_UnauthorizedBannyBody();
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
- if (IERC721(hook).ownerOf(bannyBodyId) != sender) revert Banny721TokenUriResolver_UnauthorizedBannyBody();
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
- if (_productOfTokenId({hook: hook, tokenId: bannyBodyId}).category != _BODY_CATEGORY) {
1124
- revert Banny721TokenUriResolver_BannyBodyNotBodyCategory();
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) revert Banny721TokenUriResolver_CantAccelerateTheLock();
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)) revert Banny721TokenUriResolver_UnauthorizedTransfer();
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) revert Banny721TokenUriResolver_ArrayLengthMismatch();
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 The owner of this contract can store SVG files for product IDs.
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) revert Banny721TokenUriResolver_ArrayLengthMismatch();
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) revert Banny721TokenUriResolver_ContentsAlreadyStored();
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
- if (keccak256(abi.encodePacked(svgContent)) != svgHash) revert Banny721TokenUriResolver_ContentsMismatch();
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) revert Banny721TokenUriResolver_ArrayLengthMismatch();
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)) revert Banny721TokenUriResolver_HashAlreadyStored();
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) revert Banny721TokenUriResolver_UnauthorizedBackground();
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
- if (sender != IERC721(hook).ownerOf(userId)) {
1338
- revert Banny721TokenUriResolver_UnauthorizedBackground();
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) revert Banny721TokenUriResolver_UnauthorizedOutfit();
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
- // slither-disable-next-line calls-loop
1447
- if (sender != IERC721(hook).ownerOf(wearerId)) {
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 {}