@bannynet/core-v6 0.0.11 → 0.0.13

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 (42) hide show
  1. package/ADMINISTRATION.md +42 -31
  2. package/ARCHITECTURE.md +41 -3
  3. package/AUDIT_INSTRUCTIONS.md +68 -41
  4. package/CHANGE_LOG.md +28 -7
  5. package/README.md +53 -1
  6. package/RISKS.md +33 -7
  7. package/SKILLS.md +44 -3
  8. package/STYLE_GUIDE.md +2 -2
  9. package/USER_JOURNEYS.md +327 -325
  10. package/foundry.toml +1 -1
  11. package/package.json +8 -8
  12. package/script/Add.Denver.s.sol +1 -1
  13. package/script/Deploy.s.sol +1 -1
  14. package/script/Drop1.s.sol +1 -1
  15. package/script/helpers/BannyverseDeploymentLib.sol +1 -1
  16. package/script/helpers/MigrationHelper.sol +1 -1
  17. package/src/Banny721TokenUriResolver.sol +132 -24
  18. package/test/Banny721TokenUriResolver.t.sol +1 -1
  19. package/test/BannyAttacks.t.sol +1 -1
  20. package/test/DecorateFlow.t.sol +1 -1
  21. package/test/Fork.t.sol +1 -1
  22. package/test/OutfitTransferLifecycle.t.sol +1 -1
  23. package/test/TestAuditGaps.sol +1 -1
  24. package/test/TestQALastMile.t.sol +1 -1
  25. package/test/audit/AntiStrandingRetention.t.sol +392 -0
  26. package/test/audit/MergedOutfitExclusivity.t.sol +223 -0
  27. package/test/audit/TryTransferFromStrandsAssets.t.sol +192 -0
  28. package/test/regression/ArrayLengthValidation.t.sol +1 -1
  29. package/test/regression/BodyCategoryValidation.t.sol +1 -1
  30. package/test/regression/BurnedTokenCheck.t.sol +1 -1
  31. package/test/regression/CEIReorder.t.sol +1 -1
  32. package/test/regression/ClearMetadata.t.sol +1 -1
  33. package/test/regression/MsgSenderEvents.t.sol +1 -1
  34. package/test/regression/RemovedTierDesync.t.sol +1 -1
  35. package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +0 -1809
  36. package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +0 -1795
  37. package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +0 -1810
  38. package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +0 -1796
  39. package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +0 -1795
  40. package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +0 -1810
  41. package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +0 -1796
  42. package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +0 -1795
package/foundry.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [profile.default]
2
- solc = '0.8.26'
2
+ solc = '0.8.28'
3
3
  evm_version = 'cancun'
4
4
  optimizer_runs = 200
5
5
  via_ir = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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.19",
24
- "@bananapus/core-v6": "^0.0.24",
25
- "@bananapus/permission-ids-v6": "^0.0.10",
26
- "@bananapus/router-terminal-v6": "^0.0.17",
27
- "@bananapus/suckers-v6": "^0.0.13",
28
- "@croptop/core-v6": "^0.0.20",
23
+ "@bananapus/721-hook-v6": "^0.0.21",
24
+ "@bananapus/core-v6": "^0.0.27",
25
+ "@bananapus/permission-ids-v6": "^0.0.14",
26
+ "@bananapus/router-terminal-v6": "^0.0.20",
27
+ "@bananapus/suckers-v6": "^0.0.17",
28
+ "@croptop/core-v6": "^0.0.22",
29
29
  "@openzeppelin/contracts": "^5.6.1",
30
- "@rev-net/core-v6": "^0.0.15",
30
+ "@rev-net/core-v6": "^0.0.17",
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.28;
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.28;
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.28;
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.28;
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.28;
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.28;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -1209,26 +1209,39 @@ contract Banny721TokenUriResolver is
1209
1209
  revert Banny721TokenUriResolver_UnrecognizedBackground();
1210
1210
  }
1211
1211
 
1212
- // Effects: update all state before any external transfers (CEI pattern).
1213
- _attachedBackgroundIdOf[hook][bannyBodyId] = backgroundId;
1214
- _userOf[hook][backgroundId] = bannyBodyId;
1215
-
1216
- // 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.
1217
1214
  if (userOfPreviousBackground == bannyBodyId) {
1218
- _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
+ }
1219
1222
  }
1220
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
+
1221
1228
  // Transfer the new background to this contract if it's not already owned by this contract.
1222
1229
  if (owner != address(this)) {
1223
1230
  _transferFrom({hook: hook, from: _msgSender(), to: address(this), assetId: backgroundId});
1224
1231
  }
1225
1232
  } else {
1226
- // Effects: clear the background state before any external transfer.
1227
- _attachedBackgroundIdOf[hook][bannyBodyId] = 0;
1228
-
1229
- // Interactions: try-transfer the previous background back (may have been burned).
1233
+ // Try to transfer the previous background back before clearing state.
1230
1234
  if (userOfPreviousBackground == bannyBodyId) {
1231
- _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;
1232
1245
  }
1233
1246
  }
1234
1247
  }
@@ -1335,7 +1348,17 @@ contract Banny721TokenUriResolver is
1335
1348
  // decorated.
1336
1349
  if (previousOutfitId != outfitId && wearerOf({hook: hook, outfitId: previousOutfitId}) == bannyBodyId) {
1337
1350
  // Use try-transfer: the previous outfit may have been burned or its tier removed.
1338
- _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;
1339
1362
  }
1340
1363
 
1341
1364
  if (++previousOutfitIndex < previousOutfitIds.length) {
@@ -1374,10 +1397,17 @@ contract Banny721TokenUriResolver is
1374
1397
  // decorated.
1375
1398
  // Skip outfits that are being re-equipped in the new outfit set.
1376
1399
  if (_isInArray(previousOutfitId, outfitIds)) {
1377
- // 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;
1378
1402
  } else if (wearerOf({hook: hook, outfitId: previousOutfitId}) == bannyBodyId) {
1379
1403
  // Use try-transfer: the previous outfit may have been burned or its tier removed.
1380
- _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;
1381
1411
  }
1382
1412
 
1383
1413
  if (++previousOutfitIndex < previousOutfitIds.length) {
@@ -1388,8 +1418,87 @@ contract Banny721TokenUriResolver is
1388
1418
  }
1389
1419
  }
1390
1420
 
1391
- // Store the outfits.
1392
- _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();
1393
1502
  }
1394
1503
 
1395
1504
  /// @notice Check if a value is present in an array.
@@ -1411,18 +1520,17 @@ contract Banny721TokenUriResolver is
1411
1520
  IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId});
1412
1521
  }
1413
1522
 
1414
- /// @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.
1415
1524
  /// @dev Used when returning previously equipped items that may no longer exist.
1416
- // `_tryTransferFrom` may silently fail to transfer outfit NFTs, leaving them attached to the
1417
- // Banny but owned by a different address. This is by design — the try-catch pattern prevents a single failing
1418
- // outfit transfer from blocking the entire Banny transfer. Orphaned outfits can be recovered by the original
1419
- // owner.
1420
1525
  /// @param hook The 721 contract of the token being transferred.
1421
1526
  /// @param from The address to transfer the token from.
1422
1527
  /// @param to The address to transfer the token to.
1423
1528
  /// @param assetId The ID of the token to transfer.
1424
- 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) {
1425
1531
  // slither-disable-next-line reentrancy-no-eth
1426
- 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 {}
1427
1535
  }
1428
1536
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
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.28;
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.28;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
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.28;
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.28;
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.28;
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.28;
3
3
 
4
4
  import {Test} from "forge-std/Test.sol";
5
5
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";