@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
@@ -0,0 +1,192 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
7
+
8
+ import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
9
+
10
+ contract StrandMockHook {
11
+ mapping(uint256 tokenId => address) public ownerOf;
12
+ mapping(address owner => mapping(address operator => bool)) public isApprovedForAll;
13
+
14
+ address public immutable MOCK_STORE;
15
+
16
+ constructor(address store) {
17
+ MOCK_STORE = store;
18
+ }
19
+
20
+ function STORE() external view returns (address) {
21
+ return MOCK_STORE;
22
+ }
23
+
24
+ function setOwner(uint256 tokenId, address owner) external {
25
+ ownerOf[tokenId] = owner;
26
+ }
27
+
28
+ function setApprovalForAll(address operator, bool approved) external {
29
+ isApprovedForAll[msg.sender][operator] = approved;
30
+ }
31
+
32
+ function safeTransferFrom(address from, address to, uint256 tokenId) external {
33
+ require(
34
+ msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
35
+ "StrandMockHook: not authorized"
36
+ );
37
+
38
+ ownerOf[tokenId] = to;
39
+
40
+ if (to.code.length > 0) {
41
+ bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
42
+ require(retval == IERC721Receiver.onERC721Received.selector, "StrandMockHook: receiver rejected");
43
+ }
44
+ }
45
+
46
+ function pricingContext() external pure returns (uint256, uint256, uint256) {
47
+ return (1, 18, 0);
48
+ }
49
+
50
+ function baseURI() external pure returns (string memory) {
51
+ return "ipfs://";
52
+ }
53
+ }
54
+
55
+ contract StrandMockStore {
56
+ mapping(address hook => mapping(uint256 tokenId => JB721Tier)) public tiers;
57
+
58
+ function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
59
+ tiers[hook][tokenId] = tier;
60
+ }
61
+
62
+ function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
63
+ return tiers[hook][tokenId];
64
+ }
65
+
66
+ function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
67
+ return bytes32(0);
68
+ }
69
+ }
70
+
71
+ contract NonReceiverOwner {
72
+ function approveResolver(StrandMockHook hook, address resolver) external {
73
+ hook.setApprovalForAll(resolver, true);
74
+ }
75
+
76
+ function decorate(
77
+ Banny721TokenUriResolver resolver,
78
+ address hook,
79
+ uint256 bannyBodyId,
80
+ uint256 backgroundId,
81
+ uint256[] memory outfitIds
82
+ )
83
+ external
84
+ {
85
+ resolver.decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds);
86
+ }
87
+ }
88
+
89
+ contract TryTransferFromStrandsAssetsTest is Test {
90
+ Banny721TokenUriResolver resolver;
91
+ StrandMockHook hook;
92
+ StrandMockStore store;
93
+ NonReceiverOwner ownerContract;
94
+
95
+ uint256 constant BODY_TOKEN = 4_000_000_001;
96
+ uint256 constant BACKGROUND_TOKEN = 5_000_000_001;
97
+ uint256 constant NECKLACE_TOKEN = 10_000_000_001;
98
+
99
+ function setUp() public {
100
+ store = new StrandMockStore();
101
+ hook = new StrandMockHook(address(store));
102
+ ownerContract = new NonReceiverOwner();
103
+
104
+ resolver = new Banny721TokenUriResolver(
105
+ "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", address(this), address(0)
106
+ );
107
+
108
+ _setupTier(BODY_TOKEN, 4, 0);
109
+ _setupTier(BACKGROUND_TOKEN, 5, 1);
110
+ _setupTier(NECKLACE_TOKEN, 10, 3);
111
+
112
+ hook.setOwner(BODY_TOKEN, address(ownerContract));
113
+ hook.setOwner(BACKGROUND_TOKEN, address(ownerContract));
114
+ hook.setOwner(NECKLACE_TOKEN, address(ownerContract));
115
+
116
+ ownerContract.approveResolver(hook, address(resolver));
117
+ }
118
+
119
+ function test_antiStranding_assetsRetainedWhenOwnerCannotReceiveERC721() public {
120
+ // Equip background and necklace outfit.
121
+ uint256[] memory outfits = new uint256[](1);
122
+ outfits[0] = NECKLACE_TOKEN;
123
+
124
+ ownerContract.decorate(resolver, address(hook), BODY_TOKEN, BACKGROUND_TOKEN, outfits);
125
+
126
+ assertEq(hook.ownerOf(BACKGROUND_TOKEN), address(resolver), "background should be in resolver custody");
127
+ assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "outfit should be in resolver custody");
128
+
129
+ // Try to undress --transfers back to NonReceiverOwner will fail because it doesn't implement IERC721Receiver.
130
+ uint256[] memory empty = new uint256[](0);
131
+ ownerContract.decorate(resolver, address(hook), BODY_TOKEN, 0, empty);
132
+
133
+ // Both NFTs remain in resolver custody (transfer failed silently).
134
+ assertEq(hook.ownerOf(BACKGROUND_TOKEN), address(resolver), "background remains in resolver custody");
135
+ assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "outfit remains in resolver custody");
136
+
137
+ // KEY CHANGE: State is now PRESERVED --tracking is NOT cleared on failed transfers.
138
+ // Background remains tracked because _decorateBannyWithBackground aborts the removal on failed transfer.
139
+ assertEq(
140
+ resolver.userOf(address(hook), BACKGROUND_TOKEN),
141
+ BODY_TOKEN,
142
+ "background tracking preserved -- still attached to body"
143
+ );
144
+
145
+ // Outfit is retained in the attached list because _storeOutfitsWithRetained merges failed transfers.
146
+ assertEq(
147
+ resolver.wearerOf(address(hook), NECKLACE_TOKEN),
148
+ BODY_TOKEN,
149
+ "outfit tracking preserved --still worn by body"
150
+ );
151
+
152
+ // assetIdsOf reflects the retained state.
153
+ (uint256 backgroundId, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
154
+ assertEq(backgroundId, BACKGROUND_TOKEN, "body still exposes the background");
155
+ assertEq(currentOutfits.length, 1, "body still exposes the outfit");
156
+ assertEq(currentOutfits[0], NECKLACE_TOKEN, "retained outfit is the necklace");
157
+
158
+ // The owner CAN still re-decorate because the assets are still tracked.
159
+ // Re-equipping the same outfit (no-op transfer) works fine.
160
+ outfits[0] = NECKLACE_TOKEN;
161
+ ownerContract.decorate(resolver, address(hook), BODY_TOKEN, BACKGROUND_TOKEN, outfits);
162
+
163
+ // State remains consistent.
164
+ (backgroundId, currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
165
+ assertEq(backgroundId, BACKGROUND_TOKEN, "background re-equipped");
166
+ assertEq(currentOutfits.length, 1, "outfit still attached");
167
+ assertEq(currentOutfits[0], NECKLACE_TOKEN, "outfit is still the necklace");
168
+ }
169
+
170
+ function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
171
+ JB721Tier memory tier = JB721Tier({
172
+ id: tierId,
173
+ price: 0.01 ether,
174
+ remainingSupply: 100,
175
+ initialSupply: 100,
176
+ votingUnits: 0,
177
+ reserveFrequency: 0,
178
+ reserveBeneficiary: address(0),
179
+ encodedIPFSUri: bytes32(0),
180
+ category: category,
181
+ discountPercent: 0,
182
+ allowOwnerMint: false,
183
+ transfersPausable: false,
184
+ cannotBeRemoved: false,
185
+ cannotIncreaseDiscountPercent: false,
186
+ splitPercent: 0,
187
+ resolvedUri: ""
188
+ });
189
+
190
+ store.setTier(address(hook), tokenId, tier);
191
+ }
192
+ }
@@ -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";
@@ -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";