@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.
- package/ADMINISTRATION.md +42 -31
- package/ARCHITECTURE.md +41 -3
- package/AUDIT_INSTRUCTIONS.md +68 -41
- package/CHANGE_LOG.md +28 -7
- package/README.md +53 -1
- package/RISKS.md +33 -7
- package/SKILLS.md +44 -3
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +327 -325
- package/foundry.toml +1 -1
- package/package.json +8 -8
- package/script/Add.Denver.s.sol +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/Drop1.s.sol +1 -1
- package/script/helpers/BannyverseDeploymentLib.sol +1 -1
- package/script/helpers/MigrationHelper.sol +1 -1
- package/src/Banny721TokenUriResolver.sol +132 -24
- package/test/Banny721TokenUriResolver.t.sol +1 -1
- package/test/BannyAttacks.t.sol +1 -1
- package/test/DecorateFlow.t.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/OutfitTransferLifecycle.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestQALastMile.t.sol +1 -1
- package/test/audit/AntiStrandingRetention.t.sol +392 -0
- package/test/audit/MergedOutfitExclusivity.t.sol +223 -0
- package/test/audit/TryTransferFromStrandsAssets.t.sol +192 -0
- package/test/regression/ArrayLengthValidation.t.sol +1 -1
- package/test/regression/BodyCategoryValidation.t.sol +1 -1
- package/test/regression/BurnedTokenCheck.t.sol +1 -1
- package/test/regression/CEIReorder.t.sol +1 -1
- package/test/regression/ClearMetadata.t.sol +1 -1
- package/test/regression/MsgSenderEvents.t.sol +1 -1
- package/test/regression/RemovedTierDesync.t.sol +1 -1
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +0 -1809
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +0 -1795
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +0 -1810
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +0 -1796
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +0 -1795
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +0 -1810
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +0 -1796
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +0 -1795
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
24
|
-
"@bananapus/core-v6": "^0.0.
|
|
25
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
26
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
27
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
28
|
-
"@croptop/core-v6": "^0.0.
|
|
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.
|
|
30
|
+
"@rev-net/core-v6": "^0.0.17",
|
|
31
31
|
"keccak": "^3.0.4"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
package/script/Add.Denver.s.sol
CHANGED
package/script/Deploy.s.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.
|
|
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";
|
package/script/Drop1.s.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.
|
|
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
|
-
//
|
|
1213
|
-
|
|
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({
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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}) {
|
|
1532
|
+
try IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId}) {
|
|
1533
|
+
success = true;
|
|
1534
|
+
} catch {}
|
|
1427
1535
|
}
|
|
1428
1536
|
}
|
package/test/BannyAttacks.t.sol
CHANGED
package/test/DecorateFlow.t.sol
CHANGED
package/test/Fork.t.sol
CHANGED
package/test/TestAuditGaps.sol
CHANGED