@bananapus/721-hook-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/.gas-snapshot +152 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/SKILLS.md +140 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +12 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +253 -0
- package/docs/src/SUMMARY.md +38 -0
- package/docs/src/src/JB721TiersHook.sol/contract.JB721TiersHook.md +645 -0
- package/docs/src/src/JB721TiersHookDeployer.sol/contract.JB721TiersHookDeployer.md +99 -0
- package/docs/src/src/JB721TiersHookProjectDeployer.sol/contract.JB721TiersHookProjectDeployer.md +288 -0
- package/docs/src/src/JB721TiersHookStore.sol/contract.JB721TiersHookStore.md +1096 -0
- package/docs/src/src/README.md +11 -0
- package/docs/src/src/abstract/ERC721.sol/abstract.ERC721.md +430 -0
- package/docs/src/src/abstract/JB721Hook.sol/abstract.JB721Hook.md +309 -0
- package/docs/src/src/abstract/README.md +5 -0
- package/docs/src/src/interfaces/IJB721Hook.sol/interface.IJB721Hook.md +29 -0
- package/docs/src/src/interfaces/IJB721TiersHook.sol/interface.IJB721TiersHook.md +203 -0
- package/docs/src/src/interfaces/IJB721TiersHookDeployer.sol/interface.IJB721TiersHookDeployer.md +25 -0
- package/docs/src/src/interfaces/IJB721TiersHookProjectDeployer.sol/interface.IJB721TiersHookProjectDeployer.md +64 -0
- package/docs/src/src/interfaces/IJB721TiersHookStore.sol/interface.IJB721TiersHookStore.md +265 -0
- package/docs/src/src/interfaces/IJB721TokenUriResolver.sol/interface.IJB721TokenUriResolver.md +12 -0
- package/docs/src/src/interfaces/README.md +9 -0
- package/docs/src/src/libraries/JB721Constants.sol/library.JB721Constants.md +14 -0
- package/docs/src/src/libraries/JB721TiersRulesetMetadataResolver.sol/library.JB721TiersRulesetMetadataResolver.md +68 -0
- package/docs/src/src/libraries/JBBitmap.sol/library.JBBitmap.md +82 -0
- package/docs/src/src/libraries/JBIpfsDecoder.sol/library.JBIpfsDecoder.md +61 -0
- package/docs/src/src/libraries/README.md +7 -0
- package/docs/src/src/structs/JB721InitTiersConfig.sol/struct.JB721InitTiersConfig.md +27 -0
- package/docs/src/src/structs/JB721Tier.sol/struct.JB721Tier.md +59 -0
- package/docs/src/src/structs/JB721TierConfig.sol/struct.JB721TierConfig.md +60 -0
- package/docs/src/src/structs/JB721TiersHookFlags.sol/struct.JB721TiersHookFlags.md +26 -0
- package/docs/src/src/structs/JB721TiersMintReservesConfig.sol/struct.JB721TiersMintReservesConfig.md +16 -0
- package/docs/src/src/structs/JB721TiersRulesetMetadata.sol/struct.JB721TiersRulesetMetadata.md +20 -0
- package/docs/src/src/structs/JB721TiersSetDiscountPercentConfig.sol/struct.JB721TiersSetDiscountPercentConfig.md +16 -0
- package/docs/src/src/structs/JBBitmapWord.sol/struct.JBBitmapWord.md +19 -0
- package/docs/src/src/structs/JBDeploy721TiersHookConfig.sol/struct.JBDeploy721TiersHookConfig.md +34 -0
- package/docs/src/src/structs/JBLaunchProjectConfig.sol/struct.JBLaunchProjectConfig.md +23 -0
- package/docs/src/src/structs/JBLaunchRulesetsConfig.sol/struct.JBLaunchRulesetsConfig.md +22 -0
- package/docs/src/src/structs/JBPayDataHookRulesetConfig.sol/struct.JBPayDataHookRulesetConfig.md +51 -0
- package/docs/src/src/structs/JBPayDataHookRulesetMetadata.sol/struct.JBPayDataHookRulesetMetadata.md +66 -0
- package/docs/src/src/structs/JBQueueRulesetsConfig.sol/struct.JBQueueRulesetsConfig.md +21 -0
- package/docs/src/src/structs/JBStored721Tier.sol/struct.JBStored721Tier.md +42 -0
- package/docs/src/src/structs/README.md +18 -0
- package/foundry.lock +11 -0
- package/foundry.toml +22 -0
- package/package.json +31 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +140 -0
- package/script/helpers/Hook721DeploymentLib.sol +81 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +476 -0
- package/src/JB721TiersHook.sol +765 -0
- package/src/JB721TiersHookDeployer.sol +114 -0
- package/src/JB721TiersHookProjectDeployer.sol +413 -0
- package/src/JB721TiersHookStore.sol +1195 -0
- package/src/abstract/ERC721.sol +484 -0
- package/src/abstract/JB721Hook.sol +279 -0
- package/src/interfaces/IJB721Hook.sol +21 -0
- package/src/interfaces/IJB721TiersHook.sol +135 -0
- package/src/interfaces/IJB721TiersHookDeployer.sol +22 -0
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +76 -0
- package/src/interfaces/IJB721TiersHookStore.sol +220 -0
- package/src/interfaces/IJB721TokenUriResolver.sol +10 -0
- package/src/libraries/JB721Constants.sol +7 -0
- package/src/libraries/JB721TiersRulesetMetadataResolver.sol +44 -0
- package/src/libraries/JBBitmap.sol +57 -0
- package/src/libraries/JBIpfsDecoder.sol +95 -0
- package/src/structs/JB721InitTiersConfig.sol +20 -0
- package/src/structs/JB721Tier.sol +39 -0
- package/src/structs/JB721TierConfig.sol +40 -0
- package/src/structs/JB721TiersHookFlags.sol +17 -0
- package/src/structs/JB721TiersMintReservesConfig.sol +9 -0
- package/src/structs/JB721TiersRulesetMetadata.sol +12 -0
- package/src/structs/JB721TiersSetDiscountPercentConfig.sol +9 -0
- package/src/structs/JBBitmapWord.sol +11 -0
- package/src/structs/JBDeploy721TiersHookConfig.sol +25 -0
- package/src/structs/JBLaunchProjectConfig.sol +18 -0
- package/src/structs/JBLaunchRulesetsConfig.sol +17 -0
- package/src/structs/JBPayDataHookRulesetConfig.sol +44 -0
- package/src/structs/JBPayDataHookRulesetMetadata.sol +46 -0
- package/src/structs/JBQueueRulesetsConfig.sol +13 -0
- package/src/structs/JBStored721Tier.sol +24 -0
- package/test/721HookAttacks.t.sol +396 -0
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +944 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +187 -0
- package/test/invariants/TieredHookStoreInvariant.t.sol +81 -0
- package/test/invariants/handlers/TierLifecycleHandler.sol +262 -0
- package/test/invariants/handlers/TierStoreHandler.sol +155 -0
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +141 -0
- package/test/unit/JBBitmap.t.sol +169 -0
- package/test/unit/JBIpfsDecoder.t.sol +131 -0
- package/test/unit/M6_TierSupplyCheck.t.sol +220 -0
- package/test/unit/adjustTier_Unit.t.sol +1740 -0
- package/test/unit/deployer_Unit.t.sol +103 -0
- package/test/unit/getters_constructor_Unit.t.sol +548 -0
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +443 -0
- package/test/unit/pay_Unit.t.sol +1537 -0
- package/test/unit/redeem_Unit.t.sol +459 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
5
|
+
import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
6
|
+
|
|
7
|
+
import "../../src/JB721TiersHook.sol";
|
|
8
|
+
import "../../src/JB721TiersHookProjectDeployer.sol";
|
|
9
|
+
import "../../src/JB721TiersHookDeployer.sol";
|
|
10
|
+
import "../../src/JB721TiersHookStore.sol";
|
|
11
|
+
|
|
12
|
+
import "../utils/TestBaseWorkflow.sol";
|
|
13
|
+
import "../../src/interfaces/IJB721TiersHook.sol";
|
|
14
|
+
import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
|
|
15
|
+
|
|
16
|
+
contract Test_TiersHook_E2E is TestBaseWorkflow {
|
|
17
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
18
|
+
|
|
19
|
+
uint256 totalSupplyAfterPay;
|
|
20
|
+
|
|
21
|
+
address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
|
|
22
|
+
address trustedForwarder = address(123_456);
|
|
23
|
+
|
|
24
|
+
JB721TiersHook hook;
|
|
25
|
+
|
|
26
|
+
MetadataResolverHelper metadataHelper;
|
|
27
|
+
|
|
28
|
+
event Mint(
|
|
29
|
+
uint256 indexed tokenId,
|
|
30
|
+
uint256 indexed tierId,
|
|
31
|
+
address indexed beneficiary,
|
|
32
|
+
uint256 totalAmountPaid,
|
|
33
|
+
address caller
|
|
34
|
+
);
|
|
35
|
+
event Burn(uint256 indexed tokenId, address owner, address caller);
|
|
36
|
+
|
|
37
|
+
string name = "NAME";
|
|
38
|
+
string symbol = "SYM";
|
|
39
|
+
string baseUri = "http://www.null.com/";
|
|
40
|
+
string contractUri = "ipfs://null";
|
|
41
|
+
//QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz
|
|
42
|
+
bytes32[] tokenUris = [
|
|
43
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
44
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
45
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
46
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
47
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
48
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
49
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
50
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
51
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
|
|
52
|
+
bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89)
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
JB721TiersHookProjectDeployer deployer;
|
|
56
|
+
JB721TiersHookStore store;
|
|
57
|
+
JBAddressRegistry addressRegistry;
|
|
58
|
+
|
|
59
|
+
function setUp() public override {
|
|
60
|
+
super.setUp();
|
|
61
|
+
store = new JB721TiersHookStore();
|
|
62
|
+
hook = new JB721TiersHook(jbDirectory, jbPermissions, jbRulesets, store, trustedForwarder);
|
|
63
|
+
addressRegistry = new JBAddressRegistry();
|
|
64
|
+
JB721TiersHookDeployer hookDeployer = new JB721TiersHookDeployer(hook, store, addressRegistry, trustedForwarder);
|
|
65
|
+
deployer = new JB721TiersHookProjectDeployer(
|
|
66
|
+
IJBDirectory(jbDirectory), IJBPermissions(jbPermissions), hookDeployer, address(0)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
metadataHelper = new MetadataResolverHelper();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function testLaunchProjectAndAddHookToRegistry(bytes32 salt) external {
|
|
73
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
74
|
+
createData();
|
|
75
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
76
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, bytes32(0));
|
|
77
|
+
// Check: is the first project's ID 1?
|
|
78
|
+
assertEq(projectId, 1);
|
|
79
|
+
// Check: was the hook added to the address registry?
|
|
80
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
81
|
+
assertEq(address(_hook), dataHook);
|
|
82
|
+
assertEq(addressRegistry.deployerOf(dataHook), address(deployer.HOOK_DEPLOYER()));
|
|
83
|
+
|
|
84
|
+
// Laucnh another project with a salt
|
|
85
|
+
(projectId, _hook) =
|
|
86
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
87
|
+
// Check: is the second project's ID 2?
|
|
88
|
+
assertEq(projectId, 2);
|
|
89
|
+
// Check: was the hook added to the address registry?
|
|
90
|
+
dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
91
|
+
assertEq(address(_hook), dataHook);
|
|
92
|
+
assertEq(addressRegistry.deployerOf(dataHook), address(deployer.HOOK_DEPLOYER()));
|
|
93
|
+
|
|
94
|
+
// Laucnh another project with no salt
|
|
95
|
+
(projectId, _hook) =
|
|
96
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, bytes32(0));
|
|
97
|
+
|
|
98
|
+
// Check: is the third project's ID 3?
|
|
99
|
+
assertEq(projectId, 3);
|
|
100
|
+
|
|
101
|
+
// Check: was the hook added to the address registry?
|
|
102
|
+
dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
103
|
+
assertEq(address(_hook), dataHook);
|
|
104
|
+
assertEq(addressRegistry.deployerOf(dataHook), address(deployer.HOOK_DEPLOYER()));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function testMintOnPayIfOneTierIsPassed(uint256 valueSent, bytes32 salt) external {
|
|
108
|
+
valueSent = bound(valueSent, 10, 2000);
|
|
109
|
+
// Cap the highest tier ID possible to 10.
|
|
110
|
+
uint256 highestTier = valueSent <= 100 ? (valueSent / 10) : 10;
|
|
111
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
112
|
+
createData();
|
|
113
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
114
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
115
|
+
|
|
116
|
+
// Crafting the payment metadata: add the highest tier ID.
|
|
117
|
+
uint16[] memory rawMetadata = new uint16[](1);
|
|
118
|
+
rawMetadata[0] = uint16(highestTier);
|
|
119
|
+
|
|
120
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
121
|
+
bytes[] memory data = new bytes[](1);
|
|
122
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
123
|
+
|
|
124
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
125
|
+
assertEq(address(_hook), dataHook);
|
|
126
|
+
// Pass the hook ID.
|
|
127
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
128
|
+
ids[0] = JBMetadataResolver.getId("pay", address(hook));
|
|
129
|
+
|
|
130
|
+
// Generate the metadata.
|
|
131
|
+
bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
132
|
+
|
|
133
|
+
// Check: was an NFT with the correct tier ID and token ID minted?
|
|
134
|
+
vm.expectEmit(true, true, true, true);
|
|
135
|
+
emit Mint(
|
|
136
|
+
_generateTokenId(highestTier, 1),
|
|
137
|
+
highestTier,
|
|
138
|
+
beneficiary,
|
|
139
|
+
valueSent,
|
|
140
|
+
address(jbMultiTerminal) // msg.sender
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Pay the terminal to mint the NFTs.
|
|
144
|
+
vm.prank(caller);
|
|
145
|
+
jbMultiTerminal.pay{value: valueSent}({
|
|
146
|
+
projectId: projectId,
|
|
147
|
+
amount: 100,
|
|
148
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
149
|
+
beneficiary: beneficiary,
|
|
150
|
+
minReturnedTokens: 0,
|
|
151
|
+
memo: "Take my money!",
|
|
152
|
+
metadata: hookMetadata
|
|
153
|
+
});
|
|
154
|
+
uint256 tokenId = _generateTokenId(highestTier, 1);
|
|
155
|
+
// Check: did the beneficiary receive the NFT?
|
|
156
|
+
if (valueSent < 10) {
|
|
157
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
|
|
158
|
+
} else {
|
|
159
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check: is the beneficiary the first owner of the NFT?
|
|
163
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), beneficiary);
|
|
164
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
165
|
+
|
|
166
|
+
// Check: after a transfer, are the `firstOwnerOf` and `ownerOf` still correct?
|
|
167
|
+
vm.prank(beneficiary);
|
|
168
|
+
IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId);
|
|
169
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420));
|
|
170
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
171
|
+
|
|
172
|
+
// Check: is the same true after a second transfer?
|
|
173
|
+
vm.prank(address(696_969_420));
|
|
174
|
+
IERC721(dataHook).transferFrom(address(696_969_420), address(123_456_789), tokenId);
|
|
175
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), address(123_456_789));
|
|
176
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function testFuzzMintWithDiscountOnPayIfOneTierIsPassed(uint256 tierStartPrice, uint256 discountPercent) external {
|
|
180
|
+
// Cap our fuzzed params
|
|
181
|
+
tierStartPrice = bound(tierStartPrice, 1, type(uint208).max - 1);
|
|
182
|
+
discountPercent = bound(discountPercent, 1, 200);
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
uint256 amountMinted = (tierStartPrice * 1000) / 2;
|
|
186
|
+
totalSupplyAfterPay += amountMinted;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Cap the highest tier ID.
|
|
190
|
+
uint256 highestTier = 1;
|
|
191
|
+
|
|
192
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
193
|
+
createDiscountedData(tierStartPrice, uint8(discountPercent));
|
|
194
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
195
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, bytes32(0));
|
|
196
|
+
|
|
197
|
+
// Crafting the payment metadata: add the highest tier ID.
|
|
198
|
+
uint16[] memory rawMetadata = new uint16[](1);
|
|
199
|
+
rawMetadata[0] = uint16(highestTier);
|
|
200
|
+
|
|
201
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
202
|
+
bytes[] memory data = new bytes[](1);
|
|
203
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
204
|
+
|
|
205
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
206
|
+
assertEq(address(_hook), dataHook);
|
|
207
|
+
bytes memory hookMetadata;
|
|
208
|
+
{
|
|
209
|
+
// Pass the hook ID.
|
|
210
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
211
|
+
ids[0] = JBMetadataResolver.getId("pay", address(hook));
|
|
212
|
+
|
|
213
|
+
// Generate the metadata.
|
|
214
|
+
hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* // Check: was an NFT with the correct tier ID and token ID minted?
|
|
218
|
+
vm.expectEmit(true, true, true, true);
|
|
219
|
+
emit Mint(
|
|
220
|
+
_generateTokenId(highestTier, 1),
|
|
221
|
+
highestTier,
|
|
222
|
+
beneficiary,
|
|
223
|
+
tierStartPrice,
|
|
224
|
+
address(jbMultiTerminal) // msg.sender
|
|
225
|
+
); */
|
|
226
|
+
|
|
227
|
+
if (totalSupplyAfterPay > type(uint208).max) {
|
|
228
|
+
vm.expectRevert(
|
|
229
|
+
abi.encodeWithSelector(JBTokens.JBTokens_OverflowAlert.selector, totalSupplyAfterPay, type(uint208).max)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Pay the terminal to mint the NFTs.
|
|
234
|
+
vm.deal(caller, type(uint256).max);
|
|
235
|
+
vm.prank(caller);
|
|
236
|
+
jbMultiTerminal.pay{value: tierStartPrice}({
|
|
237
|
+
projectId: projectId,
|
|
238
|
+
amount: tierStartPrice,
|
|
239
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
240
|
+
beneficiary: beneficiary,
|
|
241
|
+
minReturnedTokens: 0,
|
|
242
|
+
memo: "Take my money!",
|
|
243
|
+
metadata: hookMetadata
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (totalSupplyAfterPay < type(uint208).max) {
|
|
247
|
+
if (tierStartPrice > type(uint104).max) {
|
|
248
|
+
uint256 expectedDiscount =
|
|
249
|
+
mulDiv(uint104(tierStartPrice), discountPercent, JB721Constants.DISCOUNT_DENOMINATOR);
|
|
250
|
+
uint256 paidForNft = uint104(tierStartPrice) - expectedDiscount;
|
|
251
|
+
|
|
252
|
+
// Check: should be credited tierStartPrice minus what you paid for the NFT plus the discount
|
|
253
|
+
assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), tierStartPrice - paidForNft);
|
|
254
|
+
} else {
|
|
255
|
+
uint256 expectedCredits = mulDiv(tierStartPrice, discountPercent, JB721Constants.DISCOUNT_DENOMINATOR);
|
|
256
|
+
assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), expectedCredits);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
// Check: did the beneficiary receive the NFT?
|
|
261
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
|
|
262
|
+
|
|
263
|
+
uint256 tokenId = _generateTokenId(highestTier, 1);
|
|
264
|
+
|
|
265
|
+
// Check: is the beneficiary the first owner of the NFT?
|
|
266
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), beneficiary);
|
|
267
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
268
|
+
|
|
269
|
+
// Check: after a transfer, are the `firstOwnerOf` and `ownerOf` still correct?
|
|
270
|
+
vm.prank(beneficiary);
|
|
271
|
+
IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId);
|
|
272
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420));
|
|
273
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
274
|
+
|
|
275
|
+
// Check: is the same true after a second transfer?
|
|
276
|
+
vm.prank(address(696_969_420));
|
|
277
|
+
IERC721(dataHook).transferFrom(address(696_969_420), address(123_456_789), tokenId);
|
|
278
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), address(123_456_789));
|
|
279
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function testMintOnPayIfMultipleTiersArePassed(bytes32 salt) external {
|
|
285
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
286
|
+
createData();
|
|
287
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
288
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
289
|
+
|
|
290
|
+
// Prices of the first 5 tiers (10 * `tierId`)
|
|
291
|
+
uint256 amountNeeded = 50 + 40 + 30 + 20 + 10;
|
|
292
|
+
uint16[] memory rawMetadata = new uint16[](5);
|
|
293
|
+
|
|
294
|
+
// Mint one NFT per tier from the first 5 tiers.
|
|
295
|
+
for (uint256 i = 0; i < 5; i++) {
|
|
296
|
+
rawMetadata[i] = uint16(i + 1); // Start at `tierId` 1.
|
|
297
|
+
// Check: correct tier IDs and token IDs?
|
|
298
|
+
vm.expectEmit(true, true, true, true);
|
|
299
|
+
emit Mint(
|
|
300
|
+
_generateTokenId(i + 1, 1),
|
|
301
|
+
i + 1,
|
|
302
|
+
beneficiary,
|
|
303
|
+
amountNeeded,
|
|
304
|
+
address(jbMultiTerminal) // `msg.sender`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
309
|
+
bytes[] memory data = new bytes[](1);
|
|
310
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
311
|
+
|
|
312
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
313
|
+
assertEq(address(_hook), dataHook);
|
|
314
|
+
|
|
315
|
+
// Pass the hook ID.
|
|
316
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
317
|
+
ids[0] = JBMetadataResolver.getId("pay", address(hook));
|
|
318
|
+
|
|
319
|
+
// Generate the metadata.
|
|
320
|
+
bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
321
|
+
|
|
322
|
+
// Pay the terminal to mint the NFTs.
|
|
323
|
+
vm.prank(caller);
|
|
324
|
+
jbMultiTerminal.pay{value: amountNeeded}({
|
|
325
|
+
projectId: projectId,
|
|
326
|
+
amount: amountNeeded,
|
|
327
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
328
|
+
beneficiary: beneficiary,
|
|
329
|
+
minReturnedTokens: 0,
|
|
330
|
+
memo: "Take my money!",
|
|
331
|
+
metadata: hookMetadata
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Check: were the NFTs actually received?
|
|
335
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 5);
|
|
336
|
+
for (uint256 i = 1; i <= 5; i++) {
|
|
337
|
+
uint256 tokenId = _generateTokenId(i, 1);
|
|
338
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
339
|
+
// Check: are `firstOwnerOf` and `ownerOf` correct after a transfer?
|
|
340
|
+
vm.prank(beneficiary);
|
|
341
|
+
IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId);
|
|
342
|
+
assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420));
|
|
343
|
+
assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function testNoMintOnPayWhenNotIncludingTierIds(uint256 valueSent, bytes32 salt) external {
|
|
348
|
+
valueSent = bound(valueSent, 10, 2000);
|
|
349
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
350
|
+
createData();
|
|
351
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
352
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
353
|
+
|
|
354
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
355
|
+
assertEq(address(_hook), dataHook);
|
|
356
|
+
|
|
357
|
+
// Build the metadata with no tiers specified and the overspending flag.
|
|
358
|
+
bool allowOverspending = true;
|
|
359
|
+
uint16[] memory rawMetadata = new uint16[](0);
|
|
360
|
+
bytes memory metadata =
|
|
361
|
+
abi.encode(bytes32(0), bytes32(0), type(IJB721TiersHook).interfaceId, allowOverspending, rawMetadata);
|
|
362
|
+
|
|
363
|
+
// Pay the terminal and pass the metadata.
|
|
364
|
+
vm.prank(caller);
|
|
365
|
+
jbMultiTerminal.pay{value: valueSent}({
|
|
366
|
+
projectId: projectId,
|
|
367
|
+
amount: 100,
|
|
368
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
369
|
+
beneficiary: beneficiary,
|
|
370
|
+
minReturnedTokens: 0,
|
|
371
|
+
memo: "Take my money!",
|
|
372
|
+
metadata: metadata
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Ensure that no NFT was minted.
|
|
376
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
|
|
377
|
+
|
|
378
|
+
// Ensure the beneficiary received pay credits (since no NFTs were minted).
|
|
379
|
+
assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), valueSent);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function testNoMintOnPayWhenNotIncludingMetadata(uint256 valueSent, bytes32 salt) external {
|
|
383
|
+
valueSent = bound(valueSent, 10, 2000);
|
|
384
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
385
|
+
createData();
|
|
386
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
387
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
388
|
+
|
|
389
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
390
|
+
assertEq(address(_hook), dataHook);
|
|
391
|
+
|
|
392
|
+
// Pay the terminal with empty metadata (`bytes(0)`).
|
|
393
|
+
vm.prank(caller);
|
|
394
|
+
jbMultiTerminal.pay{value: valueSent}({
|
|
395
|
+
projectId: projectId,
|
|
396
|
+
amount: 100,
|
|
397
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
398
|
+
beneficiary: beneficiary,
|
|
399
|
+
minReturnedTokens: 0,
|
|
400
|
+
memo: "Take my money!",
|
|
401
|
+
metadata: new bytes(0)
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Ensure that no NFTs were minted.
|
|
405
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
|
|
406
|
+
|
|
407
|
+
// Ensure that the beneficiary received pay credits (since no NFTs were minted).
|
|
408
|
+
assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), valueSent);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function testMintReservedNft(uint256 valueSent, bytes32 salt) external {
|
|
412
|
+
// cheapest tier is worth 10
|
|
413
|
+
valueSent = bound(valueSent, 10, 20 ether);
|
|
414
|
+
|
|
415
|
+
// Cap the highest tier ID possible to 10.
|
|
416
|
+
uint256 highestTier = valueSent <= 100 ? valueSent / 10 : 10;
|
|
417
|
+
|
|
418
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
419
|
+
createData();
|
|
420
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
421
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
422
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
423
|
+
assertEq(address(_hook), dataHook);
|
|
424
|
+
|
|
425
|
+
// Check: Ensure no pending reserves at start (since no minting has happened).
|
|
426
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 0);
|
|
427
|
+
|
|
428
|
+
// Check: cannot mint pending reserves (since none should be pending)?
|
|
429
|
+
vm.expectRevert(
|
|
430
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector, 1, 0)
|
|
431
|
+
);
|
|
432
|
+
vm.prank(projectOwner);
|
|
433
|
+
IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1);
|
|
434
|
+
|
|
435
|
+
// Crafting the payment metadata: add the highest tier ID.
|
|
436
|
+
uint16[] memory rawMetadata = new uint16[](1);
|
|
437
|
+
rawMetadata[0] = uint16(highestTier);
|
|
438
|
+
|
|
439
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
440
|
+
bytes[] memory data = new bytes[](1);
|
|
441
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
442
|
+
|
|
443
|
+
// Pass the hook ID.
|
|
444
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
445
|
+
ids[0] = JBMetadataResolver.getId("pay", address(hook));
|
|
446
|
+
|
|
447
|
+
// Generate the metadata.
|
|
448
|
+
bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
449
|
+
|
|
450
|
+
// Check: were an NFT with the correct tier ID and token ID minted?
|
|
451
|
+
vm.expectEmit(true, true, true, true);
|
|
452
|
+
emit Mint(
|
|
453
|
+
_generateTokenId(highestTier, 1), // First one
|
|
454
|
+
highestTier,
|
|
455
|
+
beneficiary,
|
|
456
|
+
valueSent,
|
|
457
|
+
address(jbMultiTerminal) // msg.sender
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Pay the terminal to mint the NFTs.
|
|
461
|
+
vm.prank(caller);
|
|
462
|
+
jbMultiTerminal.pay{value: valueSent}({
|
|
463
|
+
projectId: projectId,
|
|
464
|
+
amount: 100,
|
|
465
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
466
|
+
beneficiary: beneficiary,
|
|
467
|
+
minReturnedTokens: 0,
|
|
468
|
+
memo: "Take my money!",
|
|
469
|
+
metadata: hookMetadata
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Check: is there now 1 pending reserve? 1 mint should yield 1 pending reserve, due to rounding up.
|
|
473
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 1);
|
|
474
|
+
|
|
475
|
+
JB721Tier memory tierBeforeMintingReserves =
|
|
476
|
+
JB721TiersHook(dataHook).STORE().tierOf(dataHook, highestTier, false);
|
|
477
|
+
|
|
478
|
+
// Mint the pending reserve NFT.
|
|
479
|
+
vm.prank(projectOwner);
|
|
480
|
+
IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1);
|
|
481
|
+
// Check: did the reserve beneficiary receive the NFT?
|
|
482
|
+
assertEq(IERC721(dataHook).balanceOf(reserveBeneficiary), 1);
|
|
483
|
+
|
|
484
|
+
JB721Tier memory tierAfterMintingReserves =
|
|
485
|
+
JB721TiersHook(dataHook).STORE().tierOf(dataHook, highestTier, false);
|
|
486
|
+
// The tier's remaining supply should have decreased by 1.
|
|
487
|
+
assertLt(tierAfterMintingReserves.remainingSupply, tierBeforeMintingReserves.remainingSupply);
|
|
488
|
+
|
|
489
|
+
// Check: there should now be 0 pending reserves.
|
|
490
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 0);
|
|
491
|
+
// Check: it should not be possible to mint pending reserves now (since there are none left).
|
|
492
|
+
vm.expectRevert(
|
|
493
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector, 1, 0)
|
|
494
|
+
);
|
|
495
|
+
vm.prank(projectOwner);
|
|
496
|
+
IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// - Mint an NFT.
|
|
500
|
+
// - Check the number of pending reserve mints available within that NFT's tier, which should be non-zero due to
|
|
501
|
+
// rounding up.
|
|
502
|
+
// - Burn an NFT from that tier.
|
|
503
|
+
// - Check the number of pending reserve mints available within the NFT's tier again.
|
|
504
|
+
// This number should be back to 0, since the NFT was burned.
|
|
505
|
+
function testCashOutToken(uint256 valueSent, bytes32 salt) external {
|
|
506
|
+
valueSent = bound(valueSent, 10, 2000);
|
|
507
|
+
|
|
508
|
+
// Cap the highest tier ID possible to 10.
|
|
509
|
+
uint256 highestTier = valueSent <= 100 ? (valueSent / 10) : 10;
|
|
510
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
511
|
+
createData();
|
|
512
|
+
|
|
513
|
+
// rr of 1.
|
|
514
|
+
tiersHookConfig.tiersConfig.tiers[highestTier - 1].reserveFrequency = 1;
|
|
515
|
+
|
|
516
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
517
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
518
|
+
|
|
519
|
+
// Craft the metadata: buy 1 NFT from the highest tier.
|
|
520
|
+
bytes memory hookMetadata;
|
|
521
|
+
bytes[] memory data;
|
|
522
|
+
bytes4[] memory ids;
|
|
523
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
524
|
+
assertEq(address(_hook), dataHook);
|
|
525
|
+
{
|
|
526
|
+
uint16[] memory rawMetadata = new uint16[](1);
|
|
527
|
+
rawMetadata[0] = uint16(highestTier);
|
|
528
|
+
|
|
529
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
530
|
+
data = new bytes[](1);
|
|
531
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
532
|
+
|
|
533
|
+
// Pass the hook ID.
|
|
534
|
+
ids = new bytes4[](1);
|
|
535
|
+
ids[0] = metadataHelper.getId("pay", address(hook));
|
|
536
|
+
|
|
537
|
+
// Generate the metadata.
|
|
538
|
+
hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Pay the terminal to mint the NFTs.
|
|
542
|
+
vm.prank(caller);
|
|
543
|
+
jbMultiTerminal.pay{value: valueSent}({
|
|
544
|
+
projectId: projectId,
|
|
545
|
+
amount: 100,
|
|
546
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
547
|
+
beneficiary: beneficiary,
|
|
548
|
+
minReturnedTokens: 0,
|
|
549
|
+
memo: "Take my money!",
|
|
550
|
+
metadata: hookMetadata
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
{
|
|
554
|
+
// Get the token ID of the NFT that was minted.
|
|
555
|
+
uint256 tokenId = _generateTokenId(highestTier, 1);
|
|
556
|
+
|
|
557
|
+
// Craft the metadata: cash out the `tokenId` which was minted.
|
|
558
|
+
uint256[] memory cashOutId = new uint256[](1);
|
|
559
|
+
cashOutId[0] = tokenId;
|
|
560
|
+
|
|
561
|
+
// Build the metadata with the tiers to cash out.
|
|
562
|
+
data[0] = abi.encode(cashOutId);
|
|
563
|
+
|
|
564
|
+
// Pass the hook ID.
|
|
565
|
+
ids[0] = metadataHelper.getId("cashOut", address(hook));
|
|
566
|
+
|
|
567
|
+
// Generate the metadata.
|
|
568
|
+
hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Check: was the beneficiary's NFT balance decreased by 1?
|
|
572
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
|
|
573
|
+
|
|
574
|
+
// Cash out the NFT.
|
|
575
|
+
vm.prank(beneficiary);
|
|
576
|
+
jbMultiTerminal.cashOutTokensOf({
|
|
577
|
+
holder: beneficiary,
|
|
578
|
+
projectId: projectId,
|
|
579
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
580
|
+
cashOutCount: 0,
|
|
581
|
+
minTokensReclaimed: 0,
|
|
582
|
+
beneficiary: payable(beneficiary),
|
|
583
|
+
metadata: hookMetadata
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Check: was the beneficiary's NFT balance decreased by 1?
|
|
587
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
|
|
588
|
+
|
|
589
|
+
// Check: was the burn accounted for in the store?
|
|
590
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfBurnedFor(dataHook, highestTier), 1);
|
|
591
|
+
|
|
592
|
+
// Check: the number of pending reserves should be equal to the calculated figure which accounts for rounding.
|
|
593
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 1);
|
|
594
|
+
|
|
595
|
+
{
|
|
596
|
+
uint16[] memory rawMetadata = new uint16[](1);
|
|
597
|
+
rawMetadata[0] = uint16(highestTier);
|
|
598
|
+
|
|
599
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
600
|
+
data = new bytes[](1);
|
|
601
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
602
|
+
|
|
603
|
+
// Pass the hook ID.
|
|
604
|
+
ids = new bytes4[](1);
|
|
605
|
+
ids[0] = metadataHelper.getId("pay", address(hook));
|
|
606
|
+
|
|
607
|
+
// Generate the metadata.
|
|
608
|
+
hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Pay the terminal to mint one more NFT.
|
|
612
|
+
vm.prank(caller);
|
|
613
|
+
jbMultiTerminal.pay{value: valueSent}({
|
|
614
|
+
projectId: projectId,
|
|
615
|
+
amount: 100,
|
|
616
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
617
|
+
beneficiary: beneficiary,
|
|
618
|
+
minReturnedTokens: 0,
|
|
619
|
+
memo: "Take my money!",
|
|
620
|
+
metadata: hookMetadata
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Check: was the beneficiary's NFT balance is 1.
|
|
624
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
|
|
625
|
+
|
|
626
|
+
// Check: the number of pending reserves shouldn't have changed.
|
|
627
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 2);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// - Mint 5 NFTs from a tier.
|
|
631
|
+
// - Check the remaining supply within that NFT's tier. (highest tier == 10, reserved percent is maximum -> 5)
|
|
632
|
+
// - Cash out all of the corresponding token from that tier
|
|
633
|
+
function testCashOutAll(bytes32 salt) external {
|
|
634
|
+
(JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
|
|
635
|
+
createData();
|
|
636
|
+
uint256 tier = 10;
|
|
637
|
+
uint256 tierPrice = tiersHookConfig.tiersConfig.tiers[tier - 1].price;
|
|
638
|
+
(uint256 projectId, IJB721TiersHook _hook) =
|
|
639
|
+
deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
|
|
640
|
+
|
|
641
|
+
// Craft the metadata: buy 5 NFTs from tier 10.
|
|
642
|
+
uint16[] memory rawMetadata = new uint16[](5);
|
|
643
|
+
for (uint256 i; i < rawMetadata.length; i++) {
|
|
644
|
+
rawMetadata[i] = uint16(tier);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
648
|
+
bytes[] memory data = new bytes[](1);
|
|
649
|
+
data[0] = abi.encode(true, rawMetadata);
|
|
650
|
+
|
|
651
|
+
address dataHook = jbRulesets.currentOf(projectId).dataHook();
|
|
652
|
+
assertEq(address(_hook), dataHook);
|
|
653
|
+
|
|
654
|
+
// Pass the hook ID.
|
|
655
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
656
|
+
ids[0] = metadataHelper.getId("pay", address(hook));
|
|
657
|
+
|
|
658
|
+
// Generate the metadata.
|
|
659
|
+
bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
660
|
+
|
|
661
|
+
// Pay the terminal to mint the NFTs.
|
|
662
|
+
vm.prank(caller);
|
|
663
|
+
jbMultiTerminal.pay{value: tierPrice * rawMetadata.length}({
|
|
664
|
+
projectId: projectId,
|
|
665
|
+
amount: 100,
|
|
666
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
667
|
+
beneficiary: beneficiary,
|
|
668
|
+
minReturnedTokens: 0,
|
|
669
|
+
memo: "Take my money!",
|
|
670
|
+
metadata: hookMetadata
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Get the beneficiary's new NFT balance.
|
|
674
|
+
uint256 nftBalance = IERC721(dataHook).balanceOf(beneficiary);
|
|
675
|
+
// Check: are the NFT balance and pending reserves correct?
|
|
676
|
+
assertEq(rawMetadata.length, nftBalance);
|
|
677
|
+
// Add 1 to the pending reserves check, as we round up for non-null values.
|
|
678
|
+
assertEq(
|
|
679
|
+
IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, tier),
|
|
680
|
+
(nftBalance / tiersHookConfig.tiersConfig.tiers[tier - 1].reserveFrequency) + 1
|
|
681
|
+
);
|
|
682
|
+
// Craft the metadata to cash out the `tokenId`s.
|
|
683
|
+
uint256[] memory cashOutId = new uint256[](5);
|
|
684
|
+
for (uint256 i; i < rawMetadata.length; i++) {
|
|
685
|
+
uint256 tokenId = _generateTokenId(tier, i + 1);
|
|
686
|
+
cashOutId[i] = tokenId;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Build the metadata with the tiers to cash out.
|
|
690
|
+
data[0] = abi.encode(cashOutId);
|
|
691
|
+
|
|
692
|
+
// Pass the hook ID.
|
|
693
|
+
ids[0] = metadataHelper.getId("cashOut", address(hook));
|
|
694
|
+
|
|
695
|
+
// Generate the metadata.
|
|
696
|
+
hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
697
|
+
|
|
698
|
+
// Cash out the NFTs.
|
|
699
|
+
vm.prank(beneficiary);
|
|
700
|
+
jbMultiTerminal.cashOutTokensOf({
|
|
701
|
+
holder: beneficiary,
|
|
702
|
+
projectId: projectId,
|
|
703
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
704
|
+
cashOutCount: 0,
|
|
705
|
+
minTokensReclaimed: 0,
|
|
706
|
+
beneficiary: payable(beneficiary),
|
|
707
|
+
metadata: hookMetadata
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Check: did the beneficiary's NFT balance decrease by 5 (to 0)?
|
|
711
|
+
assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
|
|
712
|
+
// Check: were the NFT burns accounted for in the store?
|
|
713
|
+
assertEq(IJB721TiersHook(dataHook).STORE().numberOfBurnedFor(dataHook, tier), 5);
|
|
714
|
+
// Check: did the number of pending reserves didnt change due to the burn.
|
|
715
|
+
assertEq(
|
|
716
|
+
IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, tier),
|
|
717
|
+
(nftBalance / tiersHookConfig.tiersConfig.tiers[tier - 1].reserveFrequency) + 1
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
// Craft the metadata: buy *1* NFT from tier 10.
|
|
721
|
+
uint16[] memory rawMetadata2 = new uint16[](1);
|
|
722
|
+
for (uint256 i; i < rawMetadata2.length; i++) {
|
|
723
|
+
rawMetadata2[i] = uint16(tier);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Build the metadata using the tiers to mint and the overspending flag.
|
|
727
|
+
data[0] = abi.encode(true, rawMetadata2);
|
|
728
|
+
|
|
729
|
+
// Pass the hook ID.
|
|
730
|
+
ids[0] = metadataHelper.getId("pay", address(hook));
|
|
731
|
+
|
|
732
|
+
// Generate the metadata.
|
|
733
|
+
hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
734
|
+
|
|
735
|
+
// Check: can more NFTs be minted (now that the previous ones were burned)?
|
|
736
|
+
vm.prank(caller);
|
|
737
|
+
jbMultiTerminal.pay{value: tierPrice * rawMetadata2.length}({
|
|
738
|
+
projectId: projectId,
|
|
739
|
+
amount: 100,
|
|
740
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
741
|
+
beneficiary: beneficiary,
|
|
742
|
+
minReturnedTokens: 0,
|
|
743
|
+
memo: "Take my money!",
|
|
744
|
+
metadata: hookMetadata
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Get the new NFT balance.
|
|
748
|
+
nftBalance = IERC721(dataHook).balanceOf(beneficiary);
|
|
749
|
+
// Check: are the NFT balance and pending reserves correct?
|
|
750
|
+
assertEq(rawMetadata2.length, nftBalance);
|
|
751
|
+
// Add 1 to the pending reserves check, as we round up for non-null values.
|
|
752
|
+
assertEq(
|
|
753
|
+
IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, tier),
|
|
754
|
+
(nftBalance / tiersHookConfig.tiersConfig.tiers[tier - 1].reserveFrequency) + 1
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ----- internal helpers ------
|
|
759
|
+
// Creates a `launchProjectFor(...)` payload.
|
|
760
|
+
function createData()
|
|
761
|
+
internal
|
|
762
|
+
view
|
|
763
|
+
returns (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig)
|
|
764
|
+
{
|
|
765
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](10);
|
|
766
|
+
for (uint256 i; i < 10; i++) {
|
|
767
|
+
tierConfigs[i] = JB721TierConfig({
|
|
768
|
+
price: uint104((i + 1) * 10),
|
|
769
|
+
initialSupply: uint32(10),
|
|
770
|
+
votingUnits: uint32((i + 1) * 10),
|
|
771
|
+
reserveFrequency: 10,
|
|
772
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
773
|
+
encodedIPFSUri: tokenUris[i],
|
|
774
|
+
category: uint24(100),
|
|
775
|
+
discountPercent: uint8(0),
|
|
776
|
+
allowOwnerMint: false,
|
|
777
|
+
useReserveBeneficiaryAsDefault: false,
|
|
778
|
+
transfersPausable: false,
|
|
779
|
+
useVotingUnits: false,
|
|
780
|
+
cannotBeRemoved: false,
|
|
781
|
+
cannotIncreaseDiscountPercent: false
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
tiersHookConfig = JBDeploy721TiersHookConfig({
|
|
785
|
+
name: name,
|
|
786
|
+
symbol: symbol,
|
|
787
|
+
baseUri: baseUri,
|
|
788
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
789
|
+
contractUri: contractUri,
|
|
790
|
+
tiersConfig: JB721InitTiersConfig({
|
|
791
|
+
tiers: tierConfigs,
|
|
792
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
793
|
+
decimals: 18,
|
|
794
|
+
prices: IJBPrices(address(0))
|
|
795
|
+
}),
|
|
796
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
797
|
+
flags: JB721TiersHookFlags({
|
|
798
|
+
preventOverspending: false,
|
|
799
|
+
noNewTiersWithReserves: false,
|
|
800
|
+
noNewTiersWithVotes: false,
|
|
801
|
+
noNewTiersWithOwnerMinting: true
|
|
802
|
+
})
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
JBPayDataHookRulesetMetadata memory metadata = JBPayDataHookRulesetMetadata({
|
|
806
|
+
reservedPercent: 5000, //50%
|
|
807
|
+
cashOutTaxRate: 5000, //50%
|
|
808
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
809
|
+
pausePay: false,
|
|
810
|
+
pauseCreditTransfers: false,
|
|
811
|
+
allowOwnerMinting: true,
|
|
812
|
+
allowTerminalMigration: false,
|
|
813
|
+
allowSetTerminals: false,
|
|
814
|
+
allowSetController: false,
|
|
815
|
+
ownerMustSendPayouts: false,
|
|
816
|
+
allowAddAccountingContext: false,
|
|
817
|
+
allowAddPriceFeed: false,
|
|
818
|
+
holdFees: false,
|
|
819
|
+
useTotalSurplusForCashOuts: false,
|
|
820
|
+
useDataHookForCashOut: true,
|
|
821
|
+
metadata: 0x00
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
JBPayDataHookRulesetConfig[] memory rulesetConfigurations = new JBPayDataHookRulesetConfig[](1);
|
|
825
|
+
// Package up the ruleset configuration.
|
|
826
|
+
rulesetConfigurations[0].mustStartAtOrAfter = 0;
|
|
827
|
+
rulesetConfigurations[0].duration = 14;
|
|
828
|
+
rulesetConfigurations[0].weight = 1000 * 10 ** 18;
|
|
829
|
+
rulesetConfigurations[0].weightCutPercent = 450_000_000;
|
|
830
|
+
rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
831
|
+
rulesetConfigurations[0].metadata = metadata;
|
|
832
|
+
|
|
833
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
834
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
835
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
836
|
+
token: JBConstants.NATIVE_TOKEN, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
837
|
+
});
|
|
838
|
+
terminalConfigurations[0] =
|
|
839
|
+
JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContextsToAccept});
|
|
840
|
+
|
|
841
|
+
launchProjectConfig = JBLaunchProjectConfig({
|
|
842
|
+
projectUri: projectUri,
|
|
843
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
844
|
+
terminalConfigurations: terminalConfigurations,
|
|
845
|
+
memo: ""
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function createDiscountedData(
|
|
850
|
+
uint256 _price,
|
|
851
|
+
uint8 _discountPercent
|
|
852
|
+
)
|
|
853
|
+
internal
|
|
854
|
+
view
|
|
855
|
+
returns (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig)
|
|
856
|
+
{
|
|
857
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
858
|
+
tierConfigs[0] = JB721TierConfig({
|
|
859
|
+
price: uint104(_price),
|
|
860
|
+
initialSupply: uint32(10),
|
|
861
|
+
votingUnits: uint32(10),
|
|
862
|
+
reserveFrequency: 10,
|
|
863
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
864
|
+
encodedIPFSUri: tokenUris[0],
|
|
865
|
+
category: uint24(100),
|
|
866
|
+
discountPercent: _discountPercent,
|
|
867
|
+
allowOwnerMint: false,
|
|
868
|
+
useReserveBeneficiaryAsDefault: false,
|
|
869
|
+
transfersPausable: false,
|
|
870
|
+
useVotingUnits: false,
|
|
871
|
+
cannotBeRemoved: false,
|
|
872
|
+
cannotIncreaseDiscountPercent: false
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
tiersHookConfig = JBDeploy721TiersHookConfig({
|
|
876
|
+
name: name,
|
|
877
|
+
symbol: symbol,
|
|
878
|
+
baseUri: baseUri,
|
|
879
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
880
|
+
contractUri: contractUri,
|
|
881
|
+
tiersConfig: JB721InitTiersConfig({
|
|
882
|
+
tiers: tierConfigs,
|
|
883
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
884
|
+
decimals: 18,
|
|
885
|
+
prices: IJBPrices(address(0))
|
|
886
|
+
}),
|
|
887
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
888
|
+
flags: JB721TiersHookFlags({
|
|
889
|
+
preventOverspending: false,
|
|
890
|
+
noNewTiersWithReserves: false,
|
|
891
|
+
noNewTiersWithVotes: false,
|
|
892
|
+
noNewTiersWithOwnerMinting: true
|
|
893
|
+
})
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
JBPayDataHookRulesetMetadata memory metadata = JBPayDataHookRulesetMetadata({
|
|
897
|
+
reservedPercent: 5000, //50%
|
|
898
|
+
cashOutTaxRate: 5000, //50%
|
|
899
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
900
|
+
pausePay: false,
|
|
901
|
+
pauseCreditTransfers: false,
|
|
902
|
+
allowOwnerMinting: true,
|
|
903
|
+
allowTerminalMigration: false,
|
|
904
|
+
allowSetTerminals: false,
|
|
905
|
+
allowSetController: false,
|
|
906
|
+
ownerMustSendPayouts: false,
|
|
907
|
+
allowAddAccountingContext: false,
|
|
908
|
+
allowAddPriceFeed: false,
|
|
909
|
+
holdFees: false,
|
|
910
|
+
useTotalSurplusForCashOuts: false,
|
|
911
|
+
useDataHookForCashOut: true,
|
|
912
|
+
metadata: 0x00
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
JBPayDataHookRulesetConfig[] memory rulesetConfigurations = new JBPayDataHookRulesetConfig[](1);
|
|
916
|
+
// Package up the ruleset configuration.
|
|
917
|
+
rulesetConfigurations[0].mustStartAtOrAfter = 0;
|
|
918
|
+
rulesetConfigurations[0].duration = 14;
|
|
919
|
+
rulesetConfigurations[0].weight = 1000 * 10 ** 18;
|
|
920
|
+
rulesetConfigurations[0].weightCutPercent = 450_000_000;
|
|
921
|
+
rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
922
|
+
rulesetConfigurations[0].metadata = metadata;
|
|
923
|
+
|
|
924
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
925
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
926
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
927
|
+
token: JBConstants.NATIVE_TOKEN, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
928
|
+
});
|
|
929
|
+
terminalConfigurations[0] =
|
|
930
|
+
JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContextsToAccept});
|
|
931
|
+
|
|
932
|
+
launchProjectConfig = JBLaunchProjectConfig({
|
|
933
|
+
projectUri: projectUri,
|
|
934
|
+
rulesetConfigurations: rulesetConfigurations,
|
|
935
|
+
terminalConfigurations: terminalConfigurations,
|
|
936
|
+
memo: ""
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Generate `tokenId`s based on the tier ID and token number provided.
|
|
941
|
+
function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) {
|
|
942
|
+
return (tierId * 1_000_000_000) + tokenNumber;
|
|
943
|
+
}
|
|
944
|
+
}
|