@bannynet/core-v6 0.0.28 → 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 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.29",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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;) {
@@ -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) revert Banny721TokenUriResolver_ArrayLengthMismatch();
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) revert Banny721TokenUriResolver_ContentsAlreadyStored();
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
- if (keccak256(abi.encodePacked(svgContent)) != svgHash) revert Banny721TokenUriResolver_ContentsMismatch();
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) revert Banny721TokenUriResolver_ArrayLengthMismatch();
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)) revert Banny721TokenUriResolver_HashAlreadyStored();
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) revert Banny721TokenUriResolver_UnauthorizedBackground();
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
- if (sender != IERC721(hook).ownerOf(userId)) {
1338
- revert Banny721TokenUriResolver_UnauthorizedBackground();
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) revert Banny721TokenUriResolver_UnauthorizedOutfit();
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
- // slither-disable-next-line calls-loop
1447
- if (sender != IERC721(hook).ownerOf(wearerId)) {
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 {}