@bannynet/core-v6 0.0.1
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 +53 -0
- package/SKILLS.md +94 -0
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +1809 -0
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +1795 -0
- package/foundry.toml +22 -0
- package/package.json +53 -0
- package/remappings.txt +1 -0
- package/script/1.Fix.s.sol +290 -0
- package/script/Add.Denver.s.sol +75 -0
- package/script/AirdropOutfits.s.sol +2302 -0
- package/script/Deploy.s.sol +440 -0
- package/script/Drop1.s.sol +979 -0
- package/script/MigrationContractArbitrum.sol +494 -0
- package/script/MigrationContractArbitrum1.sol +170 -0
- package/script/MigrationContractArbitrum2.sol +204 -0
- package/script/MigrationContractArbitrum3.sol +174 -0
- package/script/MigrationContractArbitrum4.sol +478 -0
- package/script/MigrationContractBase1.sol +444 -0
- package/script/MigrationContractBase2.sol +175 -0
- package/script/MigrationContractBase3.sol +309 -0
- package/script/MigrationContractBase4.sol +350 -0
- package/script/MigrationContractBase5.sol +259 -0
- package/script/MigrationContractEthereum1.sol +468 -0
- package/script/MigrationContractEthereum2.sol +306 -0
- package/script/MigrationContractEthereum3.sol +349 -0
- package/script/MigrationContractEthereum4.sol +352 -0
- package/script/MigrationContractEthereum5.sol +354 -0
- package/script/MigrationContractEthereum6.sol +270 -0
- package/script/MigrationContractEthereum7.sol +439 -0
- package/script/MigrationContractEthereum8.sol +385 -0
- package/script/MigrationContractOptimism.sol +196 -0
- package/script/helpers/BannyverseDeploymentLib.sol +73 -0
- package/script/helpers/MigrationHelper.sol +155 -0
- package/script/outfit_drop/generate-migration.js +3441 -0
- package/script/outfit_drop/raw.json +43276 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +521 -0
- package/src/Banny721TokenUriResolver.sol +1288 -0
- package/src/interfaces/IBanny721TokenUriResolver.sol +137 -0
- package/test/Banny721TokenUriResolver.t.sol +669 -0
- package/test/BannyAttacks.t.sol +322 -0
- package/test/DecorateFlow.t.sol +1056 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import {IERC721} from "@bananapus/721-hook-v5/src/abstract/ERC721.sol";
|
|
6
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHook.sol";
|
|
7
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHookStore.sol";
|
|
8
|
+
import {JB721Tier} from "@bananapus/721-hook-v5/src/structs/JB721Tier.sol";
|
|
9
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
10
|
+
|
|
11
|
+
import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
|
|
12
|
+
|
|
13
|
+
/// @notice Minimal mock hook for attack testing.
|
|
14
|
+
contract AttackMockHook {
|
|
15
|
+
mapping(uint256 tokenId => address) public ownerOf;
|
|
16
|
+
mapping(uint256 tokenId => uint32) public tierIdOf;
|
|
17
|
+
mapping(uint256 tokenId => uint24) public categoryOf;
|
|
18
|
+
address public immutable MOCK_STORE;
|
|
19
|
+
mapping(address owner => mapping(address operator => bool)) public isApprovedForAll;
|
|
20
|
+
|
|
21
|
+
constructor(address store) {
|
|
22
|
+
MOCK_STORE = store;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function STORE() external view returns (address) {
|
|
26
|
+
return MOCK_STORE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setOwner(uint256 tokenId, address _owner) external {
|
|
30
|
+
ownerOf[tokenId] = _owner;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setTier(uint256 tokenId, uint32 tierId, uint24 category) external {
|
|
34
|
+
tierIdOf[tokenId] = tierId;
|
|
35
|
+
categoryOf[tokenId] = category;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setApprovalForAll(address operator, bool approved) external {
|
|
39
|
+
isApprovedForAll[msg.sender][operator] = approved;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
43
|
+
require(
|
|
44
|
+
msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
|
|
45
|
+
"MockHook: not authorized"
|
|
46
|
+
);
|
|
47
|
+
ownerOf[tokenId] = to;
|
|
48
|
+
|
|
49
|
+
if (to.code.length > 0) {
|
|
50
|
+
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
51
|
+
require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pricingContext() external pure returns (uint256, uint256, uint256) {
|
|
56
|
+
return (1, 18, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function baseURI() external pure returns (string memory) {
|
|
60
|
+
return "ipfs://";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// @notice Minimal mock store for attack testing.
|
|
65
|
+
contract AttackMockStore {
|
|
66
|
+
mapping(address hook => mapping(uint256 tokenId => JB721Tier)) public tiers;
|
|
67
|
+
|
|
68
|
+
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
69
|
+
tiers[hook][tokenId] = tier;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
73
|
+
return tiers[hook][tokenId];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
77
|
+
return bytes32(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
81
|
+
return bytes32(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// @title BannyAttacks
|
|
86
|
+
/// @notice Adversarial security tests for Banny721TokenUriResolver decoration system.
|
|
87
|
+
contract BannyAttacks is Test {
|
|
88
|
+
Banny721TokenUriResolver resolver;
|
|
89
|
+
AttackMockHook hook;
|
|
90
|
+
AttackMockStore store;
|
|
91
|
+
|
|
92
|
+
address deployer = makeAddr("deployer");
|
|
93
|
+
address alice = makeAddr("alice");
|
|
94
|
+
address bob = makeAddr("bob");
|
|
95
|
+
address attacker = makeAddr("attacker");
|
|
96
|
+
|
|
97
|
+
// Token IDs: product ID * 1_000_000_000 + sequence.
|
|
98
|
+
// Categories: 0=Body, 1=Background, 3=Necklace, 4=Head, 5=Eyes, 7=Mouth,
|
|
99
|
+
// 9=Suit, 10=SuitBottom, 11=SuitTop
|
|
100
|
+
uint256 constant BODY_A = 4_000_000_001;
|
|
101
|
+
uint256 constant BODY_B = 4_000_000_002;
|
|
102
|
+
uint256 constant BACKGROUND = 5_000_000_001;
|
|
103
|
+
uint256 constant NECKLACE = 10_000_000_001;
|
|
104
|
+
uint256 constant HEAD = 20_000_000_001;
|
|
105
|
+
uint256 constant EYES = 30_000_000_001;
|
|
106
|
+
uint256 constant MOUTH = 40_000_000_001;
|
|
107
|
+
uint256 constant SUIT = 50_000_000_001;
|
|
108
|
+
uint256 constant SUIT_BOTTOM = 51_000_000_001;
|
|
109
|
+
uint256 constant SUIT_TOP = 52_000_000_001;
|
|
110
|
+
|
|
111
|
+
function setUp() public {
|
|
112
|
+
store = new AttackMockStore();
|
|
113
|
+
hook = new AttackMockHook(address(store));
|
|
114
|
+
|
|
115
|
+
vm.prank(deployer);
|
|
116
|
+
resolver = new Banny721TokenUriResolver(
|
|
117
|
+
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Set up tier data.
|
|
121
|
+
_setupTier(BODY_A, 4, 0);
|
|
122
|
+
_setupTier(BODY_B, 4, 0);
|
|
123
|
+
_setupTier(BACKGROUND, 5, 1);
|
|
124
|
+
_setupTier(NECKLACE, 10, 3);
|
|
125
|
+
_setupTier(HEAD, 20, 4);
|
|
126
|
+
_setupTier(EYES, 30, 5);
|
|
127
|
+
_setupTier(MOUTH, 40, 7);
|
|
128
|
+
_setupTier(SUIT, 50, 9);
|
|
129
|
+
_setupTier(SUIT_BOTTOM, 51, 10);
|
|
130
|
+
_setupTier(SUIT_TOP, 52, 11);
|
|
131
|
+
|
|
132
|
+
// Give alice all tokens.
|
|
133
|
+
hook.setOwner(BODY_A, alice);
|
|
134
|
+
hook.setOwner(BODY_B, alice);
|
|
135
|
+
hook.setOwner(BACKGROUND, alice);
|
|
136
|
+
hook.setOwner(NECKLACE, alice);
|
|
137
|
+
hook.setOwner(HEAD, alice);
|
|
138
|
+
hook.setOwner(EYES, alice);
|
|
139
|
+
hook.setOwner(MOUTH, alice);
|
|
140
|
+
hook.setOwner(SUIT, alice);
|
|
141
|
+
hook.setOwner(SUIT_BOTTOM, alice);
|
|
142
|
+
hook.setOwner(SUIT_TOP, alice);
|
|
143
|
+
|
|
144
|
+
// Approve resolver.
|
|
145
|
+
vm.prank(alice);
|
|
146
|
+
hook.setApprovalForAll(address(resolver), true);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
150
|
+
hook.setTier(tokenId, tierId, category);
|
|
151
|
+
store.setTier(
|
|
152
|
+
address(hook),
|
|
153
|
+
tokenId,
|
|
154
|
+
JB721Tier({
|
|
155
|
+
id: tierId,
|
|
156
|
+
price: 0,
|
|
157
|
+
remainingSupply: 100,
|
|
158
|
+
initialSupply: 100,
|
|
159
|
+
votingUnits: 0,
|
|
160
|
+
reserveFrequency: 0,
|
|
161
|
+
reserveBeneficiary: address(0),
|
|
162
|
+
encodedIPFSUri: bytes32(0),
|
|
163
|
+
category: category,
|
|
164
|
+
discountPercent: 0,
|
|
165
|
+
allowOwnerMint: false,
|
|
166
|
+
transfersPausable: false,
|
|
167
|
+
cannotBeRemoved: false,
|
|
168
|
+
cannotIncreaseDiscountPercent: false,
|
|
169
|
+
resolvedUri: ""
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// =========================================================================
|
|
175
|
+
// Test 1: Outfit reuse across body replacement
|
|
176
|
+
// =========================================================================
|
|
177
|
+
/// @notice Decorate body A with necklace, then try to decorate body B with
|
|
178
|
+
/// the same necklace. The necklace should be transferred back from body A
|
|
179
|
+
/// to the resolver, then attached to body B.
|
|
180
|
+
function test_outfitReuse_acrossBodies() public {
|
|
181
|
+
// Decorate body A with necklace.
|
|
182
|
+
uint256[] memory outfitsA = new uint256[](1);
|
|
183
|
+
outfitsA[0] = NECKLACE;
|
|
184
|
+
|
|
185
|
+
vm.prank(alice);
|
|
186
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfitsA);
|
|
187
|
+
|
|
188
|
+
assertEq(resolver.wearerOf(address(hook), NECKLACE), BODY_A, "Necklace on body A");
|
|
189
|
+
|
|
190
|
+
// Now decorate body B with the same necklace.
|
|
191
|
+
// Alice owns both bodies, so this should work — necklace should be removed
|
|
192
|
+
// from body A and attached to body B.
|
|
193
|
+
uint256[] memory outfitsB = new uint256[](1);
|
|
194
|
+
outfitsB[0] = NECKLACE;
|
|
195
|
+
|
|
196
|
+
vm.prank(alice);
|
|
197
|
+
resolver.decorateBannyWith(address(hook), BODY_B, 0, outfitsB);
|
|
198
|
+
|
|
199
|
+
assertEq(resolver.wearerOf(address(hook), NECKLACE), BODY_B, "Necklace should now be on body B");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =========================================================================
|
|
203
|
+
// Test 2: Lock bypass — try decorating before lock expires
|
|
204
|
+
// =========================================================================
|
|
205
|
+
/// @notice Lock a banny body, then try to change outfits before the lock expires.
|
|
206
|
+
function test_lockBypass_beforeExpiry_reverts() public {
|
|
207
|
+
// Decorate body A with necklace first.
|
|
208
|
+
uint256[] memory outfits = new uint256[](1);
|
|
209
|
+
outfits[0] = NECKLACE;
|
|
210
|
+
|
|
211
|
+
vm.prank(alice);
|
|
212
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
213
|
+
|
|
214
|
+
// Lock the outfit changes.
|
|
215
|
+
vm.prank(alice);
|
|
216
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_A);
|
|
217
|
+
|
|
218
|
+
// Try to change decoration immediately — should revert.
|
|
219
|
+
uint256[] memory newOutfits = new uint256[](1);
|
|
220
|
+
newOutfits[0] = MOUTH;
|
|
221
|
+
|
|
222
|
+
vm.prank(alice);
|
|
223
|
+
vm.expectRevert();
|
|
224
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, newOutfits);
|
|
225
|
+
|
|
226
|
+
// Fast-forward past lock period (7 days).
|
|
227
|
+
vm.warp(block.timestamp + 7 days + 1);
|
|
228
|
+
|
|
229
|
+
// Should succeed now.
|
|
230
|
+
vm.prank(alice);
|
|
231
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, newOutfits);
|
|
232
|
+
|
|
233
|
+
assertEq(resolver.wearerOf(address(hook), MOUTH), BODY_A, "Mouth should be on body A after lock");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// =========================================================================
|
|
237
|
+
// Test 3: Category conflict — head + eyes simultaneously
|
|
238
|
+
// =========================================================================
|
|
239
|
+
/// @notice Head (category 4) should conflict with Eyes (category 5).
|
|
240
|
+
function test_categoryConflict_headAndEyes_reverts() public {
|
|
241
|
+
// Try to equip both head and eyes at once.
|
|
242
|
+
uint256[] memory outfits = new uint256[](2);
|
|
243
|
+
outfits[0] = HEAD; // category 4
|
|
244
|
+
outfits[1] = EYES; // category 5
|
|
245
|
+
|
|
246
|
+
vm.prank(alice);
|
|
247
|
+
vm.expectRevert();
|
|
248
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// =========================================================================
|
|
252
|
+
// Test 4: Category conflict — suit + suit bottom
|
|
253
|
+
// =========================================================================
|
|
254
|
+
/// @notice Suit (category 9) should conflict with Suit Bottom (category 10).
|
|
255
|
+
function test_categoryConflict_suitAndParts_reverts() public {
|
|
256
|
+
uint256[] memory outfits = new uint256[](2);
|
|
257
|
+
outfits[0] = SUIT; // category 9
|
|
258
|
+
outfits[1] = SUIT_BOTTOM; // category 10
|
|
259
|
+
|
|
260
|
+
vm.prank(alice);
|
|
261
|
+
vm.expectRevert();
|
|
262
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// =========================================================================
|
|
266
|
+
// Test 5: Unauthorized decoration — non-owner tries to decorate
|
|
267
|
+
// =========================================================================
|
|
268
|
+
/// @notice Attacker (not the body owner) tries to decorate alice's banny.
|
|
269
|
+
function test_unauthorizedDecoration_reverts() public {
|
|
270
|
+
uint256[] memory outfits = new uint256[](1);
|
|
271
|
+
outfits[0] = NECKLACE;
|
|
272
|
+
|
|
273
|
+
// Give attacker a body but not the necklace owner.
|
|
274
|
+
hook.setOwner(BODY_A, attacker);
|
|
275
|
+
|
|
276
|
+
vm.prank(attacker);
|
|
277
|
+
vm.expectRevert();
|
|
278
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =========================================================================
|
|
282
|
+
// Test 6: Out-of-order categories — must be ascending
|
|
283
|
+
// =========================================================================
|
|
284
|
+
/// @notice Outfit categories must be in ascending order. Passing mouth before necklace
|
|
285
|
+
/// should revert.
|
|
286
|
+
function test_outOfOrderCategories_reverts() public {
|
|
287
|
+
uint256[] memory outfits = new uint256[](2);
|
|
288
|
+
outfits[0] = MOUTH; // category 7
|
|
289
|
+
outfits[1] = NECKLACE; // category 3 — out of order!
|
|
290
|
+
|
|
291
|
+
vm.prank(alice);
|
|
292
|
+
vm.expectRevert();
|
|
293
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// =========================================================================
|
|
297
|
+
// Test 7: Body as outfit — should revert
|
|
298
|
+
// =========================================================================
|
|
299
|
+
/// @notice Category 0 (Body) should not be usable as an outfit.
|
|
300
|
+
function test_bodyAsOutfit_reverts() public {
|
|
301
|
+
uint256[] memory outfits = new uint256[](1);
|
|
302
|
+
outfits[0] = BODY_B; // category 0
|
|
303
|
+
|
|
304
|
+
vm.prank(alice);
|
|
305
|
+
vm.expectRevert();
|
|
306
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =========================================================================
|
|
310
|
+
// Test 8: Background as outfit — should revert
|
|
311
|
+
// =========================================================================
|
|
312
|
+
/// @notice Category 1 (Background) should not be passed as outfit. It has
|
|
313
|
+
/// its own dedicated parameter in decorateBannyWith.
|
|
314
|
+
function test_backgroundAsOutfit_reverts() public {
|
|
315
|
+
uint256[] memory outfits = new uint256[](1);
|
|
316
|
+
outfits[0] = BACKGROUND; // category 1
|
|
317
|
+
|
|
318
|
+
vm.prank(alice);
|
|
319
|
+
vm.expectRevert();
|
|
320
|
+
resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
|
|
321
|
+
}
|
|
322
|
+
}
|