@bannynet/core-v6 0.0.10 → 0.0.12

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.
Files changed (41) hide show
  1. package/ADMINISTRATION.md +42 -31
  2. package/ARCHITECTURE.md +53 -13
  3. package/AUDIT_INSTRUCTIONS.md +68 -41
  4. package/CHANGE_LOG.md +28 -7
  5. package/README.md +72 -7
  6. package/RISKS.md +34 -8
  7. package/SKILLS.md +44 -3
  8. package/STYLE_GUIDE.md +1 -1
  9. package/USER_JOURNEYS.md +327 -325
  10. package/package.json +8 -8
  11. package/script/Add.Denver.s.sol +1 -1
  12. package/script/Deploy.s.sol +1 -1
  13. package/script/Drop1.s.sol +1 -1
  14. package/script/helpers/BannyverseDeploymentLib.sol +1 -1
  15. package/script/helpers/MigrationHelper.sol +1 -1
  16. package/src/Banny721TokenUriResolver.sol +148 -24
  17. package/test/Banny721TokenUriResolver.t.sol +1 -1
  18. package/test/BannyAttacks.t.sol +1 -1
  19. package/test/DecorateFlow.t.sol +32 -1
  20. package/test/Fork.t.sol +1 -1
  21. package/test/OutfitTransferLifecycle.t.sol +1 -1
  22. package/test/TestAuditGaps.sol +1 -1
  23. package/test/TestQALastMile.t.sol +1 -1
  24. package/test/audit/AntiStrandingRetention.t.sol +392 -0
  25. package/test/audit/MergedOutfitExclusivity.t.sol +223 -0
  26. package/test/audit/TryTransferFromStrandsAssets.t.sol +192 -0
  27. package/test/regression/ArrayLengthValidation.t.sol +1 -1
  28. package/test/regression/BodyCategoryValidation.t.sol +1 -1
  29. package/test/regression/BurnedTokenCheck.t.sol +1 -1
  30. package/test/regression/CEIReorder.t.sol +1 -1
  31. package/test/regression/ClearMetadata.t.sol +1 -1
  32. package/test/regression/MsgSenderEvents.t.sol +1 -1
  33. package/test/regression/RemovedTierDesync.t.sol +1 -1
  34. package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +0 -1809
  35. package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +0 -1795
  36. package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +0 -1810
  37. package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +0 -1796
  38. package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +0 -1795
  39. package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +0 -1810
  40. package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +0 -1796
  41. package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +0 -1795
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,14 @@
20
20
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
21
  },
22
22
  "dependencies": {
23
- "@bananapus/721-hook-v6": "^0.0.17",
24
- "@bananapus/core-v6": "^0.0.17",
25
- "@bananapus/permission-ids-v6": "^0.0.10",
26
- "@bananapus/router-terminal-v6": "^0.0.13",
27
- "@bananapus/suckers-v6": "^0.0.11",
28
- "@croptop/core-v6": "^0.0.18",
23
+ "@bananapus/721-hook-v6": "^0.0.20",
24
+ "@bananapus/core-v6": "^0.0.26",
25
+ "@bananapus/permission-ids-v6": "^0.0.12",
26
+ "@bananapus/router-terminal-v6": "^0.0.19",
27
+ "@bananapus/suckers-v6": "^0.0.16",
28
+ "@croptop/core-v6": "^0.0.21",
29
29
  "@openzeppelin/contracts": "^5.6.1",
30
- "@rev-net/core-v6": "^0.0.13",
30
+ "@rev-net/core-v6": "^0.0.16",
31
31
  "keccak": "^3.0.4"
32
32
  },
33
33
  "devDependencies": {
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
5
5
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Hook721Deployment, Hook721DeploymentLib} from "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
5
5
  import {CoreDeployment, CoreDeploymentLib} from "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
5
5
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {stdJson} from "forge-std/Script.sol";
5
5
  import {Vm} from "forge-std/Vm.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
5
5
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -1156,6 +1156,16 @@ contract Banny721TokenUriResolver is
1156
1156
  // ---------------------- internal transactions ---------------------- //
1157
1157
  //*********************************************************************//
1158
1158
 
1159
+ /// @notice Revert if an equipped asset is being reassigned away from a locked source body.
1160
+ /// @param hook The hook storing the assets.
1161
+ /// @param bannyBodyId The body currently using the asset.
1162
+ /// @param exemptBodyId The destination body currently being decorated.
1163
+ function _revertIfBodyLocked(address hook, uint256 bannyBodyId, uint256 exemptBodyId) internal view {
1164
+ if (bannyBodyId != 0 && bannyBodyId != exemptBodyId && outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
1165
+ revert Banny721TokenUriResolver_OutfitChangesLocked();
1166
+ }
1167
+ }
1168
+
1159
1169
  /// @notice Add a background to a banny body.
1160
1170
  /// @param hook The hook storing the assets.
1161
1171
  /// @param bannyBodyId The ID of the banny body being dressed.
@@ -1186,6 +1196,9 @@ contract Banny721TokenUriResolver is
1186
1196
  if (_msgSender() != IERC721(hook).ownerOf(userId)) {
1187
1197
  revert Banny721TokenUriResolver_UnauthorizedBackground();
1188
1198
  }
1199
+
1200
+ // A locked source body keeps its equipped background until the lock expires.
1201
+ _revertIfBodyLocked({hook: hook, bannyBodyId: userId, exemptBodyId: bannyBodyId});
1189
1202
  }
1190
1203
 
1191
1204
  // Get the background's product info.
@@ -1196,26 +1209,39 @@ contract Banny721TokenUriResolver is
1196
1209
  revert Banny721TokenUriResolver_UnrecognizedBackground();
1197
1210
  }
1198
1211
 
1199
- // Effects: update all state before any external transfers (CEI pattern).
1200
- _attachedBackgroundIdOf[hook][bannyBodyId] = backgroundId;
1201
- _userOf[hook][backgroundId] = bannyBodyId;
1202
-
1203
- // Interactions: try-transfer the previous background back (may have been burned).
1212
+ // Try to transfer the previous background back before updating state.
1213
+ // If the transfer fails, the old background stays attached to prevent NFT stranding.
1204
1214
  if (userOfPreviousBackground == bannyBodyId) {
1205
- _tryTransferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousBackgroundId});
1215
+ if (!_tryTransferFrom({
1216
+ hook: hook, from: address(this), to: _msgSender(), assetId: previousBackgroundId
1217
+ })) {
1218
+ // Transfer failed — skip the background change entirely so the old background
1219
+ // remains tracked and recoverable. The new background is not equipped.
1220
+ return;
1221
+ }
1206
1222
  }
1207
1223
 
1224
+ // Effects: update state now that the old background has been successfully returned.
1225
+ _attachedBackgroundIdOf[hook][bannyBodyId] = backgroundId;
1226
+ _userOf[hook][backgroundId] = bannyBodyId;
1227
+
1208
1228
  // Transfer the new background to this contract if it's not already owned by this contract.
1209
1229
  if (owner != address(this)) {
1210
1230
  _transferFrom({hook: hook, from: _msgSender(), to: address(this), assetId: backgroundId});
1211
1231
  }
1212
1232
  } else {
1213
- // Effects: clear the background state before any external transfer.
1214
- _attachedBackgroundIdOf[hook][bannyBodyId] = 0;
1215
-
1216
- // Interactions: try-transfer the previous background back (may have been burned).
1233
+ // Try to transfer the previous background back before clearing state.
1217
1234
  if (userOfPreviousBackground == bannyBodyId) {
1218
- _tryTransferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousBackgroundId});
1235
+ // Only clear attachment state if the transfer succeeded. If it fails (e.g. recipient rejects
1236
+ // ERC-721), the background stays attached so the owner can retry or recover.
1237
+ if (_tryTransferFrom({
1238
+ hook: hook, from: address(this), to: _msgSender(), assetId: previousBackgroundId
1239
+ })) {
1240
+ _attachedBackgroundIdOf[hook][bannyBodyId] = 0;
1241
+ }
1242
+ } else {
1243
+ // No transfer needed — just clear the stale attachment record.
1244
+ _attachedBackgroundIdOf[hook][bannyBodyId] = 0;
1219
1245
  }
1220
1246
  }
1221
1247
  }
@@ -1274,6 +1300,9 @@ contract Banny721TokenUriResolver is
1274
1300
  if (_msgSender() != IERC721(hook).ownerOf(wearerId)) {
1275
1301
  revert Banny721TokenUriResolver_UnauthorizedOutfit();
1276
1302
  }
1303
+
1304
+ // A locked source body keeps its equipped outfits until the lock expires.
1305
+ _revertIfBodyLocked({hook: hook, bannyBodyId: wearerId, exemptBodyId: bannyBodyId});
1277
1306
  }
1278
1307
 
1279
1308
  // Get the outfit's product info.
@@ -1319,7 +1348,17 @@ contract Banny721TokenUriResolver is
1319
1348
  // decorated.
1320
1349
  if (previousOutfitId != outfitId && wearerOf({hook: hook, outfitId: previousOutfitId}) == bannyBodyId) {
1321
1350
  // Use try-transfer: the previous outfit may have been burned or its tier removed.
1322
- _tryTransferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousOutfitId});
1351
+ // If transfer fails, zero is NOT written to previousOutfitIds the entry is preserved
1352
+ // so it can be retained in the attached list (preventing NFT stranding).
1353
+ if (_tryTransferFrom({
1354
+ hook: hook, from: address(this), to: _msgSender(), assetId: previousOutfitId
1355
+ })) {
1356
+ // Mark as successfully transferred so it won't be retained.
1357
+ previousOutfitIds[previousOutfitIndex] = 0;
1358
+ }
1359
+ } else {
1360
+ // Not transferring (same outfit being re-equipped or not worn by this banny) — mark as handled.
1361
+ previousOutfitIds[previousOutfitIndex] = 0;
1323
1362
  }
1324
1363
 
1325
1364
  if (++previousOutfitIndex < previousOutfitIds.length) {
@@ -1358,10 +1397,17 @@ contract Banny721TokenUriResolver is
1358
1397
  // decorated.
1359
1398
  // Skip outfits that are being re-equipped in the new outfit set.
1360
1399
  if (_isInArray(previousOutfitId, outfitIds)) {
1361
- // This outfit is being re-equipped — do not transfer it out.
1400
+ // This outfit is being re-equipped — mark as handled.
1401
+ previousOutfitIds[previousOutfitIndex] = 0;
1362
1402
  } else if (wearerOf({hook: hook, outfitId: previousOutfitId}) == bannyBodyId) {
1363
1403
  // Use try-transfer: the previous outfit may have been burned or its tier removed.
1364
- _tryTransferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousOutfitId});
1404
+ // If transfer fails, the entry stays non-zero and is retained (preventing NFT stranding).
1405
+ if (_tryTransferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousOutfitId})) {
1406
+ previousOutfitIds[previousOutfitIndex] = 0;
1407
+ }
1408
+ } else {
1409
+ // Not worn by this banny — mark as handled.
1410
+ previousOutfitIds[previousOutfitIndex] = 0;
1365
1411
  }
1366
1412
 
1367
1413
  if (++previousOutfitIndex < previousOutfitIds.length) {
@@ -1372,8 +1418,87 @@ contract Banny721TokenUriResolver is
1372
1418
  }
1373
1419
  }
1374
1420
 
1375
- // Store the outfits.
1376
- _attachedOutfitIdsOf[hook][bannyBodyId] = outfitIds;
1421
+ // Store the outfits, merging in any retained outfits whose transfers failed.
1422
+ _storeOutfitsWithRetained({
1423
+ hook: hook, bannyBodyId: bannyBodyId, outfitIds: outfitIds, previousOutfitIds: previousOutfitIds
1424
+ });
1425
+ }
1426
+
1427
+ /// @notice Store outfit IDs, merging in any retained outfits (failed transfers) from the previous list.
1428
+ /// @dev Entries in `previousOutfitIds` that are still non-zero represent outfits whose transfer back to the
1429
+ /// owner failed. These are appended to `outfitIds` so the attachment record is preserved and the owner can retry.
1430
+ /// @param hook The hook storing the assets.
1431
+ /// @param bannyBodyId The ID of the banny body being dressed.
1432
+ /// @param outfitIds The new outfit IDs to store.
1433
+ /// @param previousOutfitIds The previous outfit IDs array (zeroed entries were successfully transferred or
1434
+ /// handled).
1435
+ function _storeOutfitsWithRetained(
1436
+ address hook,
1437
+ uint256 bannyBodyId,
1438
+ uint256[] memory outfitIds,
1439
+ uint256[] memory previousOutfitIds
1440
+ )
1441
+ internal
1442
+ {
1443
+ // Count how many previous outfits failed to transfer (non-zero entries remain).
1444
+ uint256 retainedCount;
1445
+ for (uint256 i; i < previousOutfitIds.length; i++) {
1446
+ if (previousOutfitIds[i] != 0) retainedCount++;
1447
+ }
1448
+
1449
+ if (retainedCount == 0) {
1450
+ _attachedOutfitIdsOf[hook][bannyBodyId] = outfitIds;
1451
+ } else {
1452
+ // Merge new outfits with retained outfits that couldn't be transferred back.
1453
+ uint256[] memory mergedOutfitIds = new uint256[](outfitIds.length + retainedCount);
1454
+ for (uint256 i; i < outfitIds.length; i++) {
1455
+ mergedOutfitIds[i] = outfitIds[i];
1456
+ }
1457
+ uint256 mergeIndex = outfitIds.length;
1458
+ for (uint256 i; i < previousOutfitIds.length; i++) {
1459
+ if (previousOutfitIds[i] != 0) {
1460
+ mergedOutfitIds[mergeIndex++] = previousOutfitIds[i];
1461
+ }
1462
+ }
1463
+
1464
+ // Revalidate category exclusivity on the merged set. Retained outfits may conflict with the new outfits
1465
+ // (e.g., a retained HEAD outfit combined with new EYES/GLASSES/MOUTH/HEADTOP outfits).
1466
+ _validateCategoryExclusivity({hook: hook, outfitIds: mergedOutfitIds});
1467
+
1468
+ _attachedOutfitIdsOf[hook][bannyBodyId] = mergedOutfitIds;
1469
+ }
1470
+ }
1471
+
1472
+ /// @notice Validate that an array of outfit IDs does not violate category exclusivity rules.
1473
+ /// @dev HEAD is exclusive with EYES, GLASSES, MOUTH, and HEADTOP. SUIT is exclusive with SUIT_TOP and
1474
+ /// SUIT_BOTTOM. The array does not need to be sorted.
1475
+ /// @param hook The hook storing the assets.
1476
+ /// @param outfitIds The outfit IDs to validate.
1477
+ function _validateCategoryExclusivity(address hook, uint256[] memory outfitIds) internal view {
1478
+ bool hasHead;
1479
+ bool hasSuit;
1480
+ bool hasHeadAccessory;
1481
+ bool hasSuitPiece;
1482
+
1483
+ for (uint256 i; i < outfitIds.length; i++) {
1484
+ uint256 category = _productOfTokenId({hook: hook, tokenId: outfitIds[i]}).category;
1485
+
1486
+ if (category == _HEAD_CATEGORY) {
1487
+ hasHead = true;
1488
+ } else if (
1489
+ category == _EYES_CATEGORY || category == _GLASSES_CATEGORY || category == _MOUTH_CATEGORY
1490
+ || category == _HEADTOP_CATEGORY
1491
+ ) {
1492
+ hasHeadAccessory = true;
1493
+ } else if (category == _SUIT_CATEGORY) {
1494
+ hasSuit = true;
1495
+ } else if (category == _SUIT_TOP_CATEGORY || category == _SUIT_BOTTOM_CATEGORY) {
1496
+ hasSuitPiece = true;
1497
+ }
1498
+ }
1499
+
1500
+ if (hasHead && hasHeadAccessory) revert Banny721TokenUriResolver_HeadAlreadyAdded();
1501
+ if (hasSuit && hasSuitPiece) revert Banny721TokenUriResolver_SuitAlreadyAdded();
1377
1502
  }
1378
1503
 
1379
1504
  /// @notice Check if a value is present in an array.
@@ -1395,18 +1520,17 @@ contract Banny721TokenUriResolver is
1395
1520
  IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId});
1396
1521
  }
1397
1522
 
1398
- /// @notice Try to transfer a token, silently succeeding if the transfer fails (e.g. token was burned).
1523
+ /// @notice Try to transfer a token, returning whether the transfer succeeded.
1399
1524
  /// @dev Used when returning previously equipped items that may no longer exist.
1400
- // `_tryTransferFrom` may silently fail to transfer outfit NFTs, leaving them attached to the
1401
- // Banny but owned by a different address. This is by design — the try-catch pattern prevents a single failing
1402
- // outfit transfer from blocking the entire Banny transfer. Orphaned outfits can be recovered by the original
1403
- // owner.
1404
1525
  /// @param hook The 721 contract of the token being transferred.
1405
1526
  /// @param from The address to transfer the token from.
1406
1527
  /// @param to The address to transfer the token to.
1407
1528
  /// @param assetId The ID of the token to transfer.
1408
- function _tryTransferFrom(address hook, address from, address to, uint256 assetId) internal {
1529
+ /// @return success Whether the transfer succeeded.
1530
+ function _tryTransferFrom(address hook, address from, address to, uint256 assetId) internal returns (bool success) {
1409
1531
  // slither-disable-next-line reentrancy-no-eth
1410
- try IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId}) {} catch {}
1532
+ try IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId}) {
1533
+ success = true;
1534
+ } catch {}
1411
1535
  }
1412
1536
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
@@ -797,6 +797,37 @@ contract DecorateFlowTests is Test {
797
797
  assertEq(hook.ownerOf(NECKLACE_1), bob, "necklace returned to bob");
798
798
  }
799
799
 
800
+ /// @notice A locked body keeps its equipped background until the lock expires, even if the owner also controls an
801
+ /// unlocked destination body.
802
+ function test_lock_preventsMovingBackgroundFromLockedBody() public {
803
+ vm.prank(alice);
804
+ resolver.decorateBannyWith(address(hook), BODY_A, BACKGROUND_1, new uint256[](0));
805
+
806
+ vm.prank(alice);
807
+ resolver.lockOutfitChangesFor(address(hook), BODY_A);
808
+
809
+ vm.prank(alice);
810
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
811
+ resolver.decorateBannyWith(address(hook), BODY_B, BACKGROUND_1, new uint256[](0));
812
+ }
813
+
814
+ /// @notice A locked body keeps its equipped outfits until the lock expires, even if the owner also controls an
815
+ /// unlocked destination body.
816
+ function test_lock_preventsMovingOutfitFromLockedBody() public {
817
+ uint256[] memory outfits = new uint256[](1);
818
+ outfits[0] = NECKLACE_1;
819
+
820
+ vm.prank(alice);
821
+ resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
822
+
823
+ vm.prank(alice);
824
+ resolver.lockOutfitChangesFor(address(hook), BODY_A);
825
+
826
+ vm.prank(alice);
827
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
828
+ resolver.decorateBannyWith(address(hook), BODY_B, 0, outfits);
829
+ }
830
+
800
831
  // =========================================================================
801
832
  // SECTION 8: Complex Multi-Step Scenarios
802
833
  // =========================================================================
package/test/Fork.t.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";