@bannynet/core-v6 0.0.24 → 0.0.25
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 +2 -2
- package/foundry.toml +2 -1
- package/package.json +22 -12
- package/src/Banny721TokenUriResolver.sol +6 -0
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -101
- package/AUDIT_INSTRUCTIONS.md +0 -78
- package/RISKS.md +0 -80
- package/SKILLS.md +0 -42
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -190
- package/foundry.lock +0 -14
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -521
- package/test/Banny721TokenUriResolver.t.sol +0 -694
- package/test/BannyAttacks.t.sol +0 -326
- package/test/DecorateFlow.t.sol +0 -1091
- package/test/Fork.t.sol +0 -2026
- package/test/OutfitTransferLifecycle.t.sol +0 -395
- package/test/TestAuditGaps.sol +0 -724
- package/test/TestQALastMile.t.sol +0 -447
- package/test/audit/AntiStrandingRetention.t.sol +0 -422
- package/test/audit/BurnedBodyStrandsAssets.t.sol +0 -163
- package/test/audit/DuplicateCategoryRetention.t.sol +0 -163
- package/test/audit/MergedOutfitExclusivity.t.sol +0 -228
- package/test/audit/MigrationHelperVerificationBypass.t.sol +0 -102
- package/test/audit/TryTransferFromStrandsAssets.t.sol +0 -197
- package/test/regression/ArrayLengthValidation.t.sol +0 -57
- package/test/regression/BodyCategoryValidation.t.sol +0 -147
- package/test/regression/BurnedTokenCheck.t.sol +0 -186
- package/test/regression/CEIReorder.t.sol +0 -209
- package/test/regression/ClearMetadata.t.sol +0 -52
- package/test/regression/MsgSenderEvents.t.sol +0 -153
- package/test/regression/RemovedTierDesync.t.sol +0 -346
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
|
|
6
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
7
|
-
|
|
8
|
-
/// @notice Mismatched array lengths should revert.
|
|
9
|
-
contract ArrayLengthValidationTest is Test {
|
|
10
|
-
Banny721TokenUriResolver resolver;
|
|
11
|
-
address deployer = makeAddr("deployer");
|
|
12
|
-
|
|
13
|
-
function setUp() public {
|
|
14
|
-
vm.prank(deployer);
|
|
15
|
-
resolver = new Banny721TokenUriResolver(
|
|
16
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function test_setProductNames_revertsOnMismatchedLengths() public {
|
|
21
|
-
uint256[] memory upcs = new uint256[](2);
|
|
22
|
-
upcs[0] = 1;
|
|
23
|
-
upcs[1] = 2;
|
|
24
|
-
|
|
25
|
-
string[] memory names = new string[](1);
|
|
26
|
-
names[0] = "Only One";
|
|
27
|
-
|
|
28
|
-
vm.prank(deployer);
|
|
29
|
-
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
|
|
30
|
-
resolver.setProductNames(upcs, names);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function test_setSvgContentsOf_revertsOnMismatchedLengths() public {
|
|
34
|
-
uint256[] memory upcs = new uint256[](2);
|
|
35
|
-
upcs[0] = 1;
|
|
36
|
-
upcs[1] = 2;
|
|
37
|
-
|
|
38
|
-
string[] memory contents = new string[](1);
|
|
39
|
-
contents[0] = "only one";
|
|
40
|
-
|
|
41
|
-
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
|
|
42
|
-
resolver.setSvgContentsOf(upcs, contents);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function test_setSvgHashesOf_revertsOnMismatchedLengths() public {
|
|
46
|
-
uint256[] memory upcs = new uint256[](2);
|
|
47
|
-
upcs[0] = 1;
|
|
48
|
-
upcs[1] = 2;
|
|
49
|
-
|
|
50
|
-
bytes32[] memory hashes = new bytes32[](1);
|
|
51
|
-
hashes[0] = keccak256("test");
|
|
52
|
-
|
|
53
|
-
vm.prank(deployer);
|
|
54
|
-
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
|
|
55
|
-
resolver.setSvgHashesOf(upcs, hashes);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
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 {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
7
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
8
|
-
|
|
9
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
10
|
-
|
|
11
|
-
/// @notice Minimal mock hook.
|
|
12
|
-
contract MockHook57 {
|
|
13
|
-
mapping(uint256 => address) public ownerOf;
|
|
14
|
-
address public immutable MOCK_STORE;
|
|
15
|
-
mapping(address => mapping(address => bool)) public isApprovedForAll;
|
|
16
|
-
|
|
17
|
-
constructor(address store) {
|
|
18
|
-
MOCK_STORE = store;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function STORE() external view returns (address) {
|
|
22
|
-
return MOCK_STORE;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function setOwner(uint256 tokenId, address owner) external {
|
|
26
|
-
ownerOf[tokenId] = owner;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function setApprovalForAll(address operator, bool approved) external {
|
|
30
|
-
isApprovedForAll[msg.sender][operator] = approved;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
34
|
-
require(
|
|
35
|
-
msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
|
|
36
|
-
"MockHook: not authorized"
|
|
37
|
-
);
|
|
38
|
-
ownerOf[tokenId] = to;
|
|
39
|
-
if (to.code.length > 0) {
|
|
40
|
-
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
41
|
-
require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function pricingContext() external pure returns (uint256, uint256, uint256) {
|
|
46
|
-
return (1, 18, 0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function baseURI() external pure returns (string memory) {
|
|
50
|
-
return "ipfs://";
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// @notice Minimal mock store.
|
|
55
|
-
contract MockStore57 {
|
|
56
|
-
mapping(address => mapping(uint256 => 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
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
67
|
-
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
68
|
-
return bytes32(0);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/// @notice decorateBannyWith should reject non-body-category tokens as bannyBodyId.
|
|
73
|
-
contract BodyCategoryValidationTest is Test {
|
|
74
|
-
Banny721TokenUriResolver resolver;
|
|
75
|
-
MockHook57 hook;
|
|
76
|
-
MockStore57 store;
|
|
77
|
-
|
|
78
|
-
address deployer = makeAddr("deployer");
|
|
79
|
-
address alice = makeAddr("alice");
|
|
80
|
-
|
|
81
|
-
uint256 constant BODY_TOKEN = 4_000_000_001; // category 0 (body)
|
|
82
|
-
uint256 constant NECKLACE_TOKEN = 10_000_000_001; // category 3 (necklace)
|
|
83
|
-
|
|
84
|
-
function setUp() public {
|
|
85
|
-
store = new MockStore57();
|
|
86
|
-
hook = new MockHook57(address(store));
|
|
87
|
-
|
|
88
|
-
vm.prank(deployer);
|
|
89
|
-
resolver = new Banny721TokenUriResolver(
|
|
90
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Set up body token (category 0).
|
|
94
|
-
_setupTier(BODY_TOKEN, 4, 0);
|
|
95
|
-
hook.setOwner(BODY_TOKEN, alice);
|
|
96
|
-
|
|
97
|
-
// Set up necklace token (category 3) -- NOT a body.
|
|
98
|
-
_setupTier(NECKLACE_TOKEN, 10, 3);
|
|
99
|
-
hook.setOwner(NECKLACE_TOKEN, alice);
|
|
100
|
-
|
|
101
|
-
vm.prank(alice);
|
|
102
|
-
hook.setApprovalForAll(address(resolver), true);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/// @notice Passing a non-body token as bannyBodyId should revert.
|
|
106
|
-
function test_decorateBannyWith_revertsIfNotBodyCategory() public {
|
|
107
|
-
uint256[] memory outfitIds = new uint256[](0);
|
|
108
|
-
|
|
109
|
-
vm.prank(alice);
|
|
110
|
-
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_BannyBodyNotBodyCategory.selector);
|
|
111
|
-
resolver.decorateBannyWith(address(hook), NECKLACE_TOKEN, 0, outfitIds);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/// @notice Passing a valid body token should succeed.
|
|
115
|
-
function test_decorateBannyWith_succeedsWithBodyCategory() public {
|
|
116
|
-
uint256[] memory outfitIds = new uint256[](0);
|
|
117
|
-
|
|
118
|
-
vm.prank(alice);
|
|
119
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
120
|
-
// Should not revert.
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
124
|
-
JB721Tier memory tier = JB721Tier({
|
|
125
|
-
id: tierId,
|
|
126
|
-
price: 0.01 ether,
|
|
127
|
-
remainingSupply: 100,
|
|
128
|
-
initialSupply: 100,
|
|
129
|
-
votingUnits: 0,
|
|
130
|
-
reserveFrequency: 0,
|
|
131
|
-
reserveBeneficiary: address(0),
|
|
132
|
-
encodedIPFSUri: bytes32(0),
|
|
133
|
-
category: category,
|
|
134
|
-
discountPercent: 0,
|
|
135
|
-
flags: JB721TierFlags({
|
|
136
|
-
allowOwnerMint: false,
|
|
137
|
-
transfersPausable: false,
|
|
138
|
-
cantBeRemoved: false,
|
|
139
|
-
cantIncreaseDiscountPercent: false,
|
|
140
|
-
cantBuyWithCredits: false
|
|
141
|
-
}),
|
|
142
|
-
splitPercent: 0,
|
|
143
|
-
resolvedUri: ""
|
|
144
|
-
});
|
|
145
|
-
store.setTier(address(hook), tokenId, tier);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
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 {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
7
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
8
|
-
|
|
9
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
10
|
-
|
|
11
|
-
/// @notice Mock hook that supports burning (setting owner to address(0) so ownerOf reverts).
|
|
12
|
-
contract MockHook62 {
|
|
13
|
-
mapping(uint256 => address) public owners;
|
|
14
|
-
address public immutable MOCK_STORE;
|
|
15
|
-
mapping(address => mapping(address => bool)) public isApprovedForAll;
|
|
16
|
-
|
|
17
|
-
constructor(address store) {
|
|
18
|
-
MOCK_STORE = store;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function STORE() external view returns (address) {
|
|
22
|
-
return MOCK_STORE;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function setOwner(uint256 tokenId, address owner) external {
|
|
26
|
-
owners[tokenId] = owner;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function ownerOf(uint256 tokenId) external view returns (address) {
|
|
30
|
-
address owner = owners[tokenId];
|
|
31
|
-
require(owner != address(0), "ERC721: token does not exist");
|
|
32
|
-
return owner;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function burn(uint256 tokenId) external {
|
|
36
|
-
owners[tokenId] = address(0);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function setApprovalForAll(address operator, bool approved) external {
|
|
40
|
-
isApprovedForAll[msg.sender][operator] = approved;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
44
|
-
address owner = owners[tokenId];
|
|
45
|
-
require(owner != address(0), "ERC721: token does not exist");
|
|
46
|
-
require(
|
|
47
|
-
msg.sender == owner || msg.sender == from || isApprovedForAll[from][msg.sender], "MockHook: not authorized"
|
|
48
|
-
);
|
|
49
|
-
owners[tokenId] = to;
|
|
50
|
-
if (to.code.length > 0) {
|
|
51
|
-
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
52
|
-
require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function pricingContext() external pure returns (uint256, uint256, uint256) {
|
|
57
|
-
return (1, 18, 0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function baseURI() external pure returns (string memory) {
|
|
61
|
-
return "ipfs://";
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/// @notice Minimal mock store.
|
|
66
|
-
contract MockStore62 {
|
|
67
|
-
mapping(address => mapping(uint256 => JB721Tier)) public tiers;
|
|
68
|
-
|
|
69
|
-
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
70
|
-
tiers[hook][tokenId] = tier;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
74
|
-
return tiers[hook][tokenId];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
78
|
-
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
79
|
-
return bytes32(0);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/// @notice Burned equipped tokens should not lock the body.
|
|
84
|
-
contract BurnedTokenCheckTest is Test {
|
|
85
|
-
Banny721TokenUriResolver resolver;
|
|
86
|
-
MockHook62 hook;
|
|
87
|
-
MockStore62 store;
|
|
88
|
-
|
|
89
|
-
address deployer = makeAddr("deployer");
|
|
90
|
-
address alice = makeAddr("alice");
|
|
91
|
-
|
|
92
|
-
uint256 constant BODY_TOKEN = 4_000_000_001;
|
|
93
|
-
uint256 constant NECKLACE_TOKEN = 10_000_000_001;
|
|
94
|
-
uint256 constant EYES_TOKEN = 30_000_000_001;
|
|
95
|
-
|
|
96
|
-
function setUp() public {
|
|
97
|
-
store = new MockStore62();
|
|
98
|
-
hook = new MockHook62(address(store));
|
|
99
|
-
|
|
100
|
-
vm.prank(deployer);
|
|
101
|
-
resolver = new Banny721TokenUriResolver(
|
|
102
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
_setupTier(BODY_TOKEN, 4, 0);
|
|
106
|
-
_setupTier(NECKLACE_TOKEN, 10, 3);
|
|
107
|
-
_setupTier(EYES_TOKEN, 30, 5);
|
|
108
|
-
|
|
109
|
-
hook.setOwner(BODY_TOKEN, alice);
|
|
110
|
-
hook.setOwner(NECKLACE_TOKEN, alice);
|
|
111
|
-
hook.setOwner(EYES_TOKEN, alice);
|
|
112
|
-
|
|
113
|
-
vm.prank(alice);
|
|
114
|
-
hook.setApprovalForAll(address(resolver), true);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/// @notice If an equipped outfit is burned, the body should still be able to change outfits.
|
|
118
|
-
function test_decorateBannyWith_succeedsAfterEquippedOutfitBurned() public {
|
|
119
|
-
// Equip a necklace.
|
|
120
|
-
uint256[] memory outfitIds = new uint256[](1);
|
|
121
|
-
outfitIds[0] = NECKLACE_TOKEN;
|
|
122
|
-
|
|
123
|
-
vm.prank(alice);
|
|
124
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
125
|
-
|
|
126
|
-
// Verify necklace is equipped.
|
|
127
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN);
|
|
128
|
-
|
|
129
|
-
// Burn the necklace (simulate external burn while it's equipped).
|
|
130
|
-
hook.burn(NECKLACE_TOKEN);
|
|
131
|
-
|
|
132
|
-
// Now try to change outfits -- equip eyes instead. This should NOT revert.
|
|
133
|
-
uint256[] memory newOutfitIds = new uint256[](1);
|
|
134
|
-
newOutfitIds[0] = EYES_TOKEN;
|
|
135
|
-
|
|
136
|
-
vm.prank(alice);
|
|
137
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
|
|
138
|
-
|
|
139
|
-
// Verify new outfit is equipped.
|
|
140
|
-
assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/// @notice If an equipped outfit is burned, the body should be able to clear all outfits.
|
|
144
|
-
function test_decorateBannyWith_canClearOutfitsAfterBurn() public {
|
|
145
|
-
// Equip a necklace.
|
|
146
|
-
uint256[] memory outfitIds = new uint256[](1);
|
|
147
|
-
outfitIds[0] = NECKLACE_TOKEN;
|
|
148
|
-
|
|
149
|
-
vm.prank(alice);
|
|
150
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
151
|
-
|
|
152
|
-
// Burn the necklace.
|
|
153
|
-
hook.burn(NECKLACE_TOKEN);
|
|
154
|
-
|
|
155
|
-
// Clear all outfits. This should NOT revert.
|
|
156
|
-
uint256[] memory emptyOutfits = new uint256[](0);
|
|
157
|
-
|
|
158
|
-
vm.prank(alice);
|
|
159
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, emptyOutfits);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
163
|
-
JB721Tier memory tier = JB721Tier({
|
|
164
|
-
id: tierId,
|
|
165
|
-
price: 0.01 ether,
|
|
166
|
-
remainingSupply: 100,
|
|
167
|
-
initialSupply: 100,
|
|
168
|
-
votingUnits: 0,
|
|
169
|
-
reserveFrequency: 0,
|
|
170
|
-
reserveBeneficiary: address(0),
|
|
171
|
-
encodedIPFSUri: bytes32(0),
|
|
172
|
-
category: category,
|
|
173
|
-
discountPercent: 0,
|
|
174
|
-
flags: JB721TierFlags({
|
|
175
|
-
allowOwnerMint: false,
|
|
176
|
-
transfersPausable: false,
|
|
177
|
-
cantBeRemoved: false,
|
|
178
|
-
cantIncreaseDiscountPercent: false,
|
|
179
|
-
cantBuyWithCredits: false
|
|
180
|
-
}),
|
|
181
|
-
splitPercent: 0,
|
|
182
|
-
resolvedUri: ""
|
|
183
|
-
});
|
|
184
|
-
store.setTier(address(hook), tokenId, tier);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
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 {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
7
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
8
|
-
|
|
9
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
10
|
-
|
|
11
|
-
/// @notice Mock hook that records transfer ordering so we can verify CEI.
|
|
12
|
-
contract MockHookI25 {
|
|
13
|
-
mapping(uint256 => address) public ownerOf;
|
|
14
|
-
address public immutable MOCK_STORE;
|
|
15
|
-
mapping(address => mapping(address => bool)) public isApprovedForAll;
|
|
16
|
-
|
|
17
|
-
/// @notice Tracks order of transfers for CEI verification.
|
|
18
|
-
uint256[] public transferLog;
|
|
19
|
-
|
|
20
|
-
constructor(address store) {
|
|
21
|
-
MOCK_STORE = store;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function STORE() external view returns (address) {
|
|
25
|
-
return MOCK_STORE;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function setOwner(uint256 tokenId, address owner) external {
|
|
29
|
-
ownerOf[tokenId] = owner;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function setApprovalForAll(address operator, bool approved) external {
|
|
33
|
-
isApprovedForAll[msg.sender][operator] = approved;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
37
|
-
require(
|
|
38
|
-
msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
|
|
39
|
-
"MockHook: not authorized"
|
|
40
|
-
);
|
|
41
|
-
ownerOf[tokenId] = to;
|
|
42
|
-
transferLog.push(tokenId);
|
|
43
|
-
if (to.code.length > 0) {
|
|
44
|
-
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
45
|
-
require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function transferLogLength() external view returns (uint256) {
|
|
50
|
-
return transferLog.length;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function pricingContext() external pure returns (uint256, uint256, uint256) {
|
|
54
|
-
return (1, 18, 0);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function baseURI() external pure returns (string memory) {
|
|
58
|
-
return "ipfs://";
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/// @notice Minimal mock store.
|
|
63
|
-
contract MockStoreI25 {
|
|
64
|
-
mapping(address => mapping(uint256 => JB721Tier)) public tiers;
|
|
65
|
-
|
|
66
|
-
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
67
|
-
tiers[hook][tokenId] = tier;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
71
|
-
return tiers[hook][tokenId];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
75
|
-
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
76
|
-
return bytes32(0);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/// @notice _decorateBannyWithBackground follows CEI pattern.
|
|
81
|
-
/// @dev The fix reordered state writes (effects) before external transfers (interactions)
|
|
82
|
-
/// in _decorateBannyWithBackground. This test verifies that after a background replacement,
|
|
83
|
-
/// state is consistent and both the old background return and new background custody work.
|
|
84
|
-
contract CEIReorderTest is Test {
|
|
85
|
-
Banny721TokenUriResolver resolver;
|
|
86
|
-
MockHookI25 hook;
|
|
87
|
-
MockStoreI25 store;
|
|
88
|
-
|
|
89
|
-
address deployer = makeAddr("deployer");
|
|
90
|
-
address alice = makeAddr("alice");
|
|
91
|
-
|
|
92
|
-
uint256 constant BODY_TOKEN = 4_000_000_001;
|
|
93
|
-
uint256 constant BG_TOKEN_A = 5_000_000_001; // background A
|
|
94
|
-
uint256 constant BG_TOKEN_B = 5_000_000_002; // background B
|
|
95
|
-
|
|
96
|
-
function setUp() public {
|
|
97
|
-
store = new MockStoreI25();
|
|
98
|
-
hook = new MockHookI25(address(store));
|
|
99
|
-
|
|
100
|
-
vm.prank(deployer);
|
|
101
|
-
resolver = new Banny721TokenUriResolver(
|
|
102
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// Set up body (category 0).
|
|
106
|
-
_setupTier(BODY_TOKEN, 4, 0);
|
|
107
|
-
// Set up two backgrounds (category 1).
|
|
108
|
-
_setupTier(BG_TOKEN_A, 5, 1);
|
|
109
|
-
_setupTier(BG_TOKEN_B, 6, 1);
|
|
110
|
-
|
|
111
|
-
hook.setOwner(BODY_TOKEN, alice);
|
|
112
|
-
hook.setOwner(BG_TOKEN_A, alice);
|
|
113
|
-
hook.setOwner(BG_TOKEN_B, alice);
|
|
114
|
-
|
|
115
|
-
vm.prank(alice);
|
|
116
|
-
hook.setApprovalForAll(address(resolver), true);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/// @notice Replacing background A with B should update state correctly and return A to caller.
|
|
120
|
-
function test_replaceBackground_stateConsistentAfterCEIReorder() public {
|
|
121
|
-
uint256[] memory emptyOutfits = new uint256[](0);
|
|
122
|
-
|
|
123
|
-
// Attach background A.
|
|
124
|
-
vm.prank(alice);
|
|
125
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BG_TOKEN_A, emptyOutfits);
|
|
126
|
-
|
|
127
|
-
// Verify background A is attached.
|
|
128
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_A), BODY_TOKEN, "BG_A should be used by body");
|
|
129
|
-
assertEq(hook.ownerOf(BG_TOKEN_A), address(resolver), "BG_A should be held by resolver");
|
|
130
|
-
|
|
131
|
-
// Replace background A with B.
|
|
132
|
-
vm.prank(alice);
|
|
133
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BG_TOKEN_B, emptyOutfits);
|
|
134
|
-
|
|
135
|
-
// Verify state is consistent after replacement.
|
|
136
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_B), BODY_TOKEN, "BG_B should now be used by body");
|
|
137
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_A), 0, "BG_A should no longer be used");
|
|
138
|
-
assertEq(hook.ownerOf(BG_TOKEN_B), address(resolver), "BG_B should be held by resolver");
|
|
139
|
-
assertEq(hook.ownerOf(BG_TOKEN_A), alice, "BG_A should be returned to alice");
|
|
140
|
-
|
|
141
|
-
// Verify assetIdsOf reflects the new background.
|
|
142
|
-
(uint256 backgroundId,) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
143
|
-
assertEq(backgroundId, BG_TOKEN_B, "assetIdsOf should show BG_B");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/// @notice Clearing a background (setting to 0) should update state before transfer.
|
|
147
|
-
function test_clearBackground_stateConsistentAfterCEIReorder() public {
|
|
148
|
-
uint256[] memory emptyOutfits = new uint256[](0);
|
|
149
|
-
|
|
150
|
-
// Attach background A.
|
|
151
|
-
vm.prank(alice);
|
|
152
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BG_TOKEN_A, emptyOutfits);
|
|
153
|
-
|
|
154
|
-
// Clear background.
|
|
155
|
-
vm.prank(alice);
|
|
156
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, emptyOutfits);
|
|
157
|
-
|
|
158
|
-
// State should be cleared and token returned.
|
|
159
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_A), 0, "BG_A should no longer be used");
|
|
160
|
-
assertEq(hook.ownerOf(BG_TOKEN_A), alice, "BG_A should be returned to alice");
|
|
161
|
-
|
|
162
|
-
(uint256 backgroundId,) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
163
|
-
assertEq(backgroundId, 0, "assetIdsOf should show no background");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/// @notice Setting the same background again should be a no-op (no redundant transfers).
|
|
167
|
-
function test_sameBackground_noRedundantTransfer() public {
|
|
168
|
-
uint256[] memory emptyOutfits = new uint256[](0);
|
|
169
|
-
|
|
170
|
-
// Attach background A.
|
|
171
|
-
vm.prank(alice);
|
|
172
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BG_TOKEN_A, emptyOutfits);
|
|
173
|
-
|
|
174
|
-
uint256 logBefore = hook.transferLogLength();
|
|
175
|
-
|
|
176
|
-
// Re-attach same background.
|
|
177
|
-
vm.prank(alice);
|
|
178
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BG_TOKEN_A, emptyOutfits);
|
|
179
|
-
|
|
180
|
-
// No new transfers should have occurred for the background.
|
|
181
|
-
uint256 logAfter = hook.transferLogLength();
|
|
182
|
-
assertEq(logAfter, logBefore, "No transfers should occur when re-attaching same background");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
186
|
-
JB721Tier memory tier = JB721Tier({
|
|
187
|
-
id: tierId,
|
|
188
|
-
price: 0.01 ether,
|
|
189
|
-
remainingSupply: 100,
|
|
190
|
-
initialSupply: 100,
|
|
191
|
-
votingUnits: 0,
|
|
192
|
-
reserveFrequency: 0,
|
|
193
|
-
reserveBeneficiary: address(0),
|
|
194
|
-
encodedIPFSUri: bytes32(0),
|
|
195
|
-
category: category,
|
|
196
|
-
discountPercent: 0,
|
|
197
|
-
flags: JB721TierFlags({
|
|
198
|
-
allowOwnerMint: false,
|
|
199
|
-
transfersPausable: false,
|
|
200
|
-
cantBeRemoved: false,
|
|
201
|
-
cantIncreaseDiscountPercent: false,
|
|
202
|
-
cantBuyWithCredits: false
|
|
203
|
-
}),
|
|
204
|
-
splitPercent: 0,
|
|
205
|
-
resolvedUri: ""
|
|
206
|
-
});
|
|
207
|
-
store.setTier(address(hook), tokenId, tier);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
|
|
6
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
7
|
-
|
|
8
|
-
/// @notice setMetadata should allow clearing fields to empty string.
|
|
9
|
-
contract ClearMetadataTest is Test {
|
|
10
|
-
Banny721TokenUriResolver resolver;
|
|
11
|
-
address deployer = makeAddr("deployer");
|
|
12
|
-
|
|
13
|
-
function setUp() public {
|
|
14
|
-
vm.prank(deployer);
|
|
15
|
-
resolver = new Banny721TokenUriResolver(
|
|
16
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function test_setMetadata_canClearToEmpty() public {
|
|
21
|
-
// First, set some values.
|
|
22
|
-
vm.prank(deployer);
|
|
23
|
-
resolver.setMetadata("Initial desc", "https://initial.url", "https://initial.base/");
|
|
24
|
-
|
|
25
|
-
assertEq(resolver.svgDescription(), "Initial desc");
|
|
26
|
-
assertEq(resolver.svgExternalUrl(), "https://initial.url");
|
|
27
|
-
assertEq(resolver.svgBaseUri(), "https://initial.base/");
|
|
28
|
-
|
|
29
|
-
// Now clear all fields by passing empty strings.
|
|
30
|
-
vm.prank(deployer);
|
|
31
|
-
resolver.setMetadata("", "", "");
|
|
32
|
-
|
|
33
|
-
// Fields should now be empty (cleared), not still holding old values.
|
|
34
|
-
assertEq(resolver.svgDescription(), "", "description should be cleared");
|
|
35
|
-
assertEq(resolver.svgExternalUrl(), "", "url should be cleared");
|
|
36
|
-
assertEq(resolver.svgBaseUri(), "", "baseUri should be cleared");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function test_setMetadata_canClearIndividualField() public {
|
|
40
|
-
// Set all fields.
|
|
41
|
-
vm.prank(deployer);
|
|
42
|
-
resolver.setMetadata("desc", "https://url", "https://base/");
|
|
43
|
-
|
|
44
|
-
// Clear only description, keep others.
|
|
45
|
-
vm.prank(deployer);
|
|
46
|
-
resolver.setMetadata("", "https://url", "https://base/");
|
|
47
|
-
|
|
48
|
-
assertEq(resolver.svgDescription(), "", "description should be cleared");
|
|
49
|
-
assertEq(resolver.svgExternalUrl(), "https://url", "url should be unchanged");
|
|
50
|
-
assertEq(resolver.svgBaseUri(), "https://base/", "baseUri should be unchanged");
|
|
51
|
-
}
|
|
52
|
-
}
|