@bananapus/721-hook-v6 0.0.1 → 0.0.2
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/package.json +1 -1
- package/src/JB721TiersHook.sol +236 -146
- package/src/JB721TiersHookDeployer.sol +1 -0
- package/src/JB721TiersHookStore.sol +17 -4
- package/src/interfaces/IJB721TiersHook.sol +17 -2
- package/src/libraries/JB721TiersHookLib.sol +336 -0
- package/src/structs/JB721Tier.sol +3 -0
- package/src/structs/JB721TierConfig.sol +8 -0
- package/src/structs/JBStored721Tier.sol +5 -4
- package/test/721HookAttacks.t.sol +6 -2
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +6 -2
- package/test/invariants/handlers/TierLifecycleHandler.sol +3 -1
- package/test/invariants/handlers/TierStoreHandler.sol +4 -1
- package/test/unit/adjustTier_Unit.t.sol +75 -22
- package/test/unit/getters_constructor_Unit.t.sol +14 -9
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +12 -12
- package/test/unit/pay_Unit.t.sol +6 -10
- package/test/unit/redeem_Unit.t.sol +13 -11
- package/test/unit/tierSplitRouting_Unit.t.sol +275 -0
- package/src/abstract/JB721Hook.sol +0 -279
- package/src/interfaces/IJB721Hook.sol +0 -21
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "../utils/UnitTestSetup.sol";
|
|
5
|
+
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
6
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
7
|
+
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
8
|
+
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
9
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
10
|
+
|
|
11
|
+
contract Test_TierSplitRouting is UnitTestSetup {
|
|
12
|
+
using stdStorage for StdStorage;
|
|
13
|
+
|
|
14
|
+
address alice = makeAddr("alice");
|
|
15
|
+
address bob = makeAddr("bob");
|
|
16
|
+
address mockSplits = makeAddr("mockSplits");
|
|
17
|
+
|
|
18
|
+
function setUp() public override {
|
|
19
|
+
super.setUp();
|
|
20
|
+
vm.etch(mockSplits, new bytes(0x69));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper: build a tier config with splits.
|
|
24
|
+
function _tierConfigWithSplit(
|
|
25
|
+
uint104 price,
|
|
26
|
+
uint32 splitPercent
|
|
27
|
+
)
|
|
28
|
+
internal
|
|
29
|
+
pure
|
|
30
|
+
returns (JB721TierConfig memory config)
|
|
31
|
+
{
|
|
32
|
+
config.price = price;
|
|
33
|
+
config.initialSupply = uint32(100);
|
|
34
|
+
config.category = uint24(1);
|
|
35
|
+
config.encodedIPFSUri = bytes32(uint256(0x1234));
|
|
36
|
+
config.splitPercent = splitPercent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper: build payer metadata for tier IDs.
|
|
40
|
+
function _buildPayerMetadata(
|
|
41
|
+
address hookAddress,
|
|
42
|
+
uint16[] memory tierIdsToMint
|
|
43
|
+
)
|
|
44
|
+
internal
|
|
45
|
+
view
|
|
46
|
+
returns (bytes memory)
|
|
47
|
+
{
|
|
48
|
+
bytes[] memory data = new bytes[](1);
|
|
49
|
+
data[0] = abi.encode(false, tierIdsToMint);
|
|
50
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
51
|
+
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
52
|
+
return metadataHelper.createMetadata(ids, data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ──────────────────────────────────────────────
|
|
56
|
+
// Test: beforePayRecordedWith calculates split amount
|
|
57
|
+
// ──────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function test_beforePayRecorded_calculatesSplitAmount() public {
|
|
60
|
+
// Create hook with a default tier (no splits).
|
|
61
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
62
|
+
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
63
|
+
|
|
64
|
+
// Add a tier with 50% split directly to the hook's store.
|
|
65
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
66
|
+
tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000); // 50%
|
|
67
|
+
vm.prank(address(testHook));
|
|
68
|
+
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
69
|
+
|
|
70
|
+
// Build payer metadata requesting that tier.
|
|
71
|
+
uint16[] memory mintIds = new uint16[](1);
|
|
72
|
+
mintIds[0] = uint16(tierIds[0]);
|
|
73
|
+
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
74
|
+
|
|
75
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
76
|
+
terminal: mockTerminalAddress,
|
|
77
|
+
payer: beneficiary,
|
|
78
|
+
amount: JBTokenAmount({
|
|
79
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
80
|
+
value: 1 ether,
|
|
81
|
+
decimals: 18,
|
|
82
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
83
|
+
}),
|
|
84
|
+
projectId: projectId,
|
|
85
|
+
rulesetId: 0,
|
|
86
|
+
beneficiary: beneficiary,
|
|
87
|
+
weight: 10e18,
|
|
88
|
+
reservedPercent: 5000,
|
|
89
|
+
metadata: payerMetadata
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
(uint256 weight, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
93
|
+
|
|
94
|
+
// Weight unchanged.
|
|
95
|
+
assertEq(weight, 10e18);
|
|
96
|
+
// Hook spec should forward 50% of 1 ETH = 0.5 ETH.
|
|
97
|
+
assertEq(specs.length, 1);
|
|
98
|
+
assertEq(specs[0].amount, 0.5 ether);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ──────────────────────────────────────────────
|
|
102
|
+
// Test: no splitPercent means no forwarded amount
|
|
103
|
+
// ──────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function test_beforePayRecorded_noSplitPercent_noForwardedAmount() public {
|
|
106
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
107
|
+
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
108
|
+
|
|
109
|
+
// Add a tier with 0% split.
|
|
110
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
111
|
+
tierConfigs[0] = _tierConfigWithSplit(1 ether, 0);
|
|
112
|
+
vm.prank(address(testHook));
|
|
113
|
+
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
114
|
+
|
|
115
|
+
uint16[] memory mintIds = new uint16[](1);
|
|
116
|
+
mintIds[0] = uint16(tierIds[0]);
|
|
117
|
+
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
118
|
+
|
|
119
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
120
|
+
terminal: mockTerminalAddress,
|
|
121
|
+
payer: beneficiary,
|
|
122
|
+
amount: JBTokenAmount({
|
|
123
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
124
|
+
value: 1 ether,
|
|
125
|
+
decimals: 18,
|
|
126
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
127
|
+
}),
|
|
128
|
+
projectId: projectId,
|
|
129
|
+
rulesetId: 0,
|
|
130
|
+
beneficiary: beneficiary,
|
|
131
|
+
weight: 10e18,
|
|
132
|
+
reservedPercent: 5000,
|
|
133
|
+
metadata: payerMetadata
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
137
|
+
|
|
138
|
+
// No split amount forwarded.
|
|
139
|
+
assertEq(specs[0].amount, 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ──────────────────────────────────────────────
|
|
143
|
+
// Test: multiple tiers with different split percents
|
|
144
|
+
// ──────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function test_beforePayRecorded_multipleTiersDifferentSplitPercents() public {
|
|
147
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
148
|
+
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
149
|
+
|
|
150
|
+
// Tier 1: 1 ETH, 30% split. Tier 2: 2 ETH, 100% split.
|
|
151
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](2);
|
|
152
|
+
tierConfigs[0] = _tierConfigWithSplit(1 ether, 300_000_000);
|
|
153
|
+
tierConfigs[0].category = 1;
|
|
154
|
+
tierConfigs[1] = _tierConfigWithSplit(2 ether, 1_000_000_000);
|
|
155
|
+
tierConfigs[1].category = 2;
|
|
156
|
+
vm.prank(address(testHook));
|
|
157
|
+
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
158
|
+
|
|
159
|
+
uint16[] memory mintIds = new uint16[](2);
|
|
160
|
+
mintIds[0] = uint16(tierIds[0]);
|
|
161
|
+
mintIds[1] = uint16(tierIds[1]);
|
|
162
|
+
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
163
|
+
|
|
164
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
165
|
+
terminal: mockTerminalAddress,
|
|
166
|
+
payer: beneficiary,
|
|
167
|
+
amount: JBTokenAmount({
|
|
168
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
169
|
+
value: 3 ether,
|
|
170
|
+
decimals: 18,
|
|
171
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
172
|
+
}),
|
|
173
|
+
projectId: projectId,
|
|
174
|
+
rulesetId: 0,
|
|
175
|
+
beneficiary: beneficiary,
|
|
176
|
+
weight: 10e18,
|
|
177
|
+
reservedPercent: 5000,
|
|
178
|
+
metadata: payerMetadata
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
182
|
+
|
|
183
|
+
// Total split = 1 ETH * 30% + 2 ETH * 100% = 0.3 + 2.0 = 2.3 ETH.
|
|
184
|
+
assertEq(specs[0].amount, 2.3 ether);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ──────────────────────────────────────────────
|
|
188
|
+
// Test: afterPayRecordedWith distributes to split beneficiary
|
|
189
|
+
// ──────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
function test_afterPayRecorded_distributesToBeneficiary() public {
|
|
192
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
193
|
+
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
194
|
+
|
|
195
|
+
// Add a tier with 50% split.
|
|
196
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
197
|
+
tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000);
|
|
198
|
+
vm.prank(address(testHook));
|
|
199
|
+
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
200
|
+
|
|
201
|
+
// Mock directory checks.
|
|
202
|
+
mockAndExpect(
|
|
203
|
+
address(mockJBDirectory),
|
|
204
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
205
|
+
abi.encode(true)
|
|
206
|
+
);
|
|
207
|
+
mockAndExpect(
|
|
208
|
+
address(mockJBDirectory),
|
|
209
|
+
abi.encodeWithSelector(IJBDirectory.controllerOf.selector, projectId),
|
|
210
|
+
abi.encode(mockJBController)
|
|
211
|
+
);
|
|
212
|
+
mockAndExpect(mockJBController, abi.encodeWithSelector(IJBController.SPLITS.selector), abi.encode(mockSplits));
|
|
213
|
+
|
|
214
|
+
// Mock splits: alice gets 100%.
|
|
215
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
216
|
+
splits[0] = JBSplit({
|
|
217
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
218
|
+
projectId: 0,
|
|
219
|
+
beneficiary: payable(alice),
|
|
220
|
+
preferAddToBalance: false,
|
|
221
|
+
lockedUntil: 0,
|
|
222
|
+
hook: IJBSplitHook(address(0))
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
226
|
+
mockAndExpect(
|
|
227
|
+
mockSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Build payer metadata.
|
|
231
|
+
uint16[] memory mintIds = new uint16[](1);
|
|
232
|
+
mintIds[0] = uint16(tierIds[0]);
|
|
233
|
+
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
234
|
+
|
|
235
|
+
// Build hook metadata (per-tier split breakdown from beforePayRecordedWith).
|
|
236
|
+
uint16[] memory splitTierIds = new uint16[](1);
|
|
237
|
+
splitTierIds[0] = uint16(tierIds[0]);
|
|
238
|
+
uint256[] memory splitAmounts = new uint256[](1);
|
|
239
|
+
splitAmounts[0] = 0.5 ether;
|
|
240
|
+
|
|
241
|
+
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
242
|
+
payer: beneficiary,
|
|
243
|
+
projectId: projectId,
|
|
244
|
+
rulesetId: 0,
|
|
245
|
+
amount: JBTokenAmount({
|
|
246
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
247
|
+
value: 1 ether,
|
|
248
|
+
decimals: 18,
|
|
249
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
250
|
+
}),
|
|
251
|
+
forwardedAmount: JBTokenAmount({
|
|
252
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
253
|
+
value: 0.5 ether,
|
|
254
|
+
decimals: 18,
|
|
255
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
256
|
+
}),
|
|
257
|
+
weight: 10e18,
|
|
258
|
+
newlyIssuedTokenCount: 0,
|
|
259
|
+
beneficiary: beneficiary,
|
|
260
|
+
hookMetadata: abi.encode(splitTierIds, splitAmounts),
|
|
261
|
+
payerMetadata: payerMetadata
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
uint256 aliceBalanceBefore = alice.balance;
|
|
265
|
+
|
|
266
|
+
vm.deal(mockTerminalAddress, 1 ether);
|
|
267
|
+
vm.prank(mockTerminalAddress);
|
|
268
|
+
testHook.afterPayRecordedWith{value: 0.5 ether}(payContext);
|
|
269
|
+
|
|
270
|
+
// Alice should have received 0.5 ETH.
|
|
271
|
+
assertEq(alice.balance - aliceBalanceBefore, 0.5 ether);
|
|
272
|
+
// NFT should have been minted.
|
|
273
|
+
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.23;
|
|
3
|
-
|
|
4
|
-
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
5
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
-
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
7
|
-
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
8
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
9
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
10
|
-
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
11
|
-
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
12
|
-
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
13
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
14
|
-
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
15
|
-
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
16
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
17
|
-
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
18
|
-
import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
|
|
19
|
-
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
20
|
-
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
21
|
-
|
|
22
|
-
import {ERC721} from "./ERC721.sol";
|
|
23
|
-
import {IJB721Hook} from "../interfaces/IJB721Hook.sol";
|
|
24
|
-
|
|
25
|
-
/// @title JB721Hook
|
|
26
|
-
/// @notice When a project which uses this hook is paid, this hook may mint NFTs to the payer, depending on this hook's
|
|
27
|
-
/// setup, the amount paid, and information specified by the payer. The project's owner can enable NFT cash outs.
|
|
28
|
-
/// through this hook, allowing the NFT holders to burn their NFTs to reclaim funds from the project (in proportion to
|
|
29
|
-
/// the NFT's price).
|
|
30
|
-
abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
31
|
-
//*********************************************************************//
|
|
32
|
-
// --------------------------- custom errors ------------------------- //
|
|
33
|
-
//*********************************************************************//
|
|
34
|
-
|
|
35
|
-
error JB721Hook_InvalidCashOut();
|
|
36
|
-
error JB721Hook_InvalidPay();
|
|
37
|
-
error JB721Hook_UnauthorizedToken(uint256 tokenId, address holder);
|
|
38
|
-
error JB721Hook_UnexpectedTokenCashedOut();
|
|
39
|
-
|
|
40
|
-
//*********************************************************************//
|
|
41
|
-
// --------------- public immutable stored properties ---------------- //
|
|
42
|
-
//*********************************************************************//
|
|
43
|
-
|
|
44
|
-
/// @notice The directory of terminals and controllers for projects.
|
|
45
|
-
IJBDirectory public immutable override DIRECTORY;
|
|
46
|
-
|
|
47
|
-
/// @notice The ID used when parsing metadata.
|
|
48
|
-
address public immutable override METADATA_ID_TARGET;
|
|
49
|
-
|
|
50
|
-
//*********************************************************************//
|
|
51
|
-
// -------------------- public stored properties --------------------- //
|
|
52
|
-
//*********************************************************************//
|
|
53
|
-
|
|
54
|
-
/// @notice The ID of the project that this contract is associated with.
|
|
55
|
-
uint256 public override PROJECT_ID;
|
|
56
|
-
|
|
57
|
-
//*********************************************************************//
|
|
58
|
-
// -------------------------- constructor ---------------------------- //
|
|
59
|
-
//*********************************************************************//
|
|
60
|
-
|
|
61
|
-
/// @param directory A directory of terminals and controllers for projects.
|
|
62
|
-
constructor(IJBDirectory directory) {
|
|
63
|
-
DIRECTORY = directory;
|
|
64
|
-
// Store the address of the original hook deploy. Clones will each use the address of the instance they're based
|
|
65
|
-
// on.
|
|
66
|
-
METADATA_ID_TARGET = address(this);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
//*********************************************************************//
|
|
70
|
-
// ------------------------- external views -------------------------- //
|
|
71
|
-
//*********************************************************************//
|
|
72
|
-
|
|
73
|
-
/// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
|
|
74
|
-
/// terminal's `pay(...)` transaction.
|
|
75
|
-
/// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
|
|
76
|
-
/// @param context The payment context passed to this contract by the `pay(...)` function.
|
|
77
|
-
/// @return weight The new `weight` to use, overriding the ruleset's `weight`.
|
|
78
|
-
/// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
|
|
79
|
-
/// terminal's balance.
|
|
80
|
-
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
81
|
-
public
|
|
82
|
-
view
|
|
83
|
-
virtual
|
|
84
|
-
override
|
|
85
|
-
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
86
|
-
{
|
|
87
|
-
// Forward the received weight and memo, and use this contract as the only pay hook.
|
|
88
|
-
weight = context.weight;
|
|
89
|
-
hookSpecifications = new JBPayHookSpecification[](1);
|
|
90
|
-
hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
|
|
94
|
-
/// terminal's `cashOutTokensOf(...)` transaction.
|
|
95
|
-
/// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
|
|
96
|
-
/// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
|
|
97
|
-
/// `useDataHookForCashOut` set to `true`.
|
|
98
|
-
/// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
|
|
99
|
-
/// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
|
|
100
|
-
/// @return cashOutCount The amount of tokens that should be considered cashed out.
|
|
101
|
-
/// @return totalSupply The total amount of tokens that are considered to be existing.
|
|
102
|
-
/// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
|
|
103
|
-
/// the beneficiary.
|
|
104
|
-
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
105
|
-
public
|
|
106
|
-
view
|
|
107
|
-
virtual
|
|
108
|
-
override
|
|
109
|
-
returns (
|
|
110
|
-
uint256 cashOutTaxRate,
|
|
111
|
-
uint256 cashOutCount,
|
|
112
|
-
uint256 totalSupply,
|
|
113
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
114
|
-
)
|
|
115
|
-
{
|
|
116
|
-
// Make sure (fungible) project tokens aren't also being cashed out.
|
|
117
|
-
if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut();
|
|
118
|
-
|
|
119
|
-
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
120
|
-
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
121
|
-
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Use this contract as the only cash out hook.
|
|
125
|
-
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
126
|
-
hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
127
|
-
|
|
128
|
-
uint256[] memory decodedTokenIds;
|
|
129
|
-
|
|
130
|
-
// Decode the metadata.
|
|
131
|
-
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
132
|
-
|
|
133
|
-
// Use the cash out weight of the provided 721s.
|
|
134
|
-
cashOutCount = cashOutWeightOf({tokenIds: decodedTokenIds, context: context});
|
|
135
|
-
|
|
136
|
-
// Use the total cash out weight of the 721s.
|
|
137
|
-
totalSupply = totalCashOutWeight(context);
|
|
138
|
-
|
|
139
|
-
// Use the cash out tax rate from the context.
|
|
140
|
-
cashOutTaxRate = context.cashOutTaxRate;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
|
|
144
|
-
function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
//*********************************************************************//
|
|
149
|
-
// -------------------------- public views --------------------------- //
|
|
150
|
-
//*********************************************************************//
|
|
151
|
-
|
|
152
|
-
/// @notice Returns the cumulative cash out weight of the specified token IDs relative to the
|
|
153
|
-
/// `totalCashOutWeight`.
|
|
154
|
-
/// @param tokenIds The NFT token IDs to calculate the cumulative cash out weight of.
|
|
155
|
-
/// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
|
|
156
|
-
/// @return The cumulative cash out weight of the specified token IDs.
|
|
157
|
-
function cashOutWeightOf(
|
|
158
|
-
uint256[] memory tokenIds,
|
|
159
|
-
JBBeforeCashOutRecordedContext calldata context
|
|
160
|
-
)
|
|
161
|
-
public
|
|
162
|
-
view
|
|
163
|
-
virtual
|
|
164
|
-
returns (uint256)
|
|
165
|
-
{
|
|
166
|
-
tokenIds; // Prevents unused var compiler and natspec complaints.
|
|
167
|
-
context; // Prevents unused var compiler and natspec complaints.
|
|
168
|
-
return 0;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/// @notice Indicates if this contract adheres to the specified interface.
|
|
172
|
-
/// @dev See {IERC165-supportsInterface}.
|
|
173
|
-
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
174
|
-
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
|
|
175
|
-
return interfaceId == type(IJB721Hook).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
176
|
-
|| interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
|
|
177
|
-
|| interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/// @notice Calculates the cumulative cash out weight of all NFT token IDs.
|
|
181
|
-
/// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
|
|
182
|
-
/// @return The total cumulative cash out weight of all NFT token IDs.
|
|
183
|
-
function totalCashOutWeight(JBBeforeCashOutRecordedContext calldata context) public view virtual returns (uint256) {
|
|
184
|
-
context; // Prevents unused var compiler and natspec complaints.
|
|
185
|
-
return 0;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
//*********************************************************************//
|
|
189
|
-
// ---------------------- external transactions ---------------------- //
|
|
190
|
-
//*********************************************************************//
|
|
191
|
-
|
|
192
|
-
/// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
|
|
193
|
-
/// `IJBPayHook`.
|
|
194
|
-
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
195
|
-
/// @param context The payment context passed in by the terminal.
|
|
196
|
-
// slither-disable-next-line locked-ether
|
|
197
|
-
function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
|
|
198
|
-
uint256 projectId = PROJECT_ID;
|
|
199
|
-
|
|
200
|
-
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
201
|
-
// interaction with the correct project.
|
|
202
|
-
if (
|
|
203
|
-
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
204
|
-
|| context.projectId != projectId
|
|
205
|
-
) revert JB721Hook_InvalidPay();
|
|
206
|
-
|
|
207
|
-
// Process the payment.
|
|
208
|
-
_processPayment(context);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
|
|
212
|
-
/// `context.beneficiary`. Part of `IJBCashOutHook`.
|
|
213
|
-
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
214
|
-
/// @param context The cash out context passed in by the terminal.
|
|
215
|
-
// slither-disable-next-line locked-ether
|
|
216
|
-
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
|
|
217
|
-
external
|
|
218
|
-
payable
|
|
219
|
-
virtual
|
|
220
|
-
override
|
|
221
|
-
{
|
|
222
|
-
// Keep a reference to the project ID.
|
|
223
|
-
uint256 projectId = PROJECT_ID;
|
|
224
|
-
|
|
225
|
-
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
226
|
-
// interaction with the correct project.
|
|
227
|
-
if (
|
|
228
|
-
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
229
|
-
|| context.projectId != projectId
|
|
230
|
-
) revert JB721Hook_InvalidCashOut();
|
|
231
|
-
|
|
232
|
-
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
233
|
-
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
234
|
-
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
|
|
235
|
-
metadata: context.cashOutMetadata
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
uint256[] memory decodedTokenIds;
|
|
239
|
-
|
|
240
|
-
// Decode the metadata.
|
|
241
|
-
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
242
|
-
|
|
243
|
-
// Iterate through the NFTs, burning them if the owner is correct.
|
|
244
|
-
for (uint256 i; i < decodedTokenIds.length; i++) {
|
|
245
|
-
// Set the current NFT's token ID.
|
|
246
|
-
uint256 tokenId = decodedTokenIds[i];
|
|
247
|
-
|
|
248
|
-
// Make sure the token's owner is correct.
|
|
249
|
-
if (_ownerOf(tokenId) != context.holder) revert JB721Hook_UnauthorizedToken(tokenId, context.holder);
|
|
250
|
-
|
|
251
|
-
// Burn the token.
|
|
252
|
-
_burn(tokenId);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Call the hook.
|
|
256
|
-
_didBurn(decodedTokenIds);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
//*********************************************************************//
|
|
260
|
-
// ---------------------- internal transactions ---------------------- //
|
|
261
|
-
//*********************************************************************//
|
|
262
|
-
|
|
263
|
-
/// @notice Initializes the contract by associating it with a project and adding ERC721 details.
|
|
264
|
-
/// @param projectId The ID of the project that this contract is associated with.
|
|
265
|
-
/// @param name The name of the NFT collection.
|
|
266
|
-
/// @param symbol The symbol representing the NFT collection.
|
|
267
|
-
function _initialize(uint256 projectId, string memory name, string memory symbol) internal {
|
|
268
|
-
ERC721._initialize({name_: name, symbol_: symbol});
|
|
269
|
-
PROJECT_ID = projectId;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/// @notice Executes after NFTs have been burned via cash out.
|
|
273
|
-
/// @param tokenIds The token IDs of the NFTs that were burned.
|
|
274
|
-
function _didBurn(uint256[] memory tokenIds) internal virtual;
|
|
275
|
-
|
|
276
|
-
/// @notice Process a received payment.
|
|
277
|
-
/// @param context The payment context passed in by the terminal.
|
|
278
|
-
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual;
|
|
279
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity ^0.8.0;
|
|
3
|
-
|
|
4
|
-
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
5
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
-
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
7
|
-
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
8
|
-
|
|
9
|
-
interface IJB721Hook is IJBRulesetDataHook, IJBPayHook, IJBCashOutHook {
|
|
10
|
-
/// @notice The directory of terminals and controllers for projects.
|
|
11
|
-
/// @return The directory contract.
|
|
12
|
-
function DIRECTORY() external view returns (IJBDirectory);
|
|
13
|
-
|
|
14
|
-
/// @notice The ID used when parsing metadata.
|
|
15
|
-
/// @return The address of the metadata ID target.
|
|
16
|
-
function METADATA_ID_TARGET() external view returns (address);
|
|
17
|
-
|
|
18
|
-
/// @notice The ID of the project that this contract is associated with.
|
|
19
|
-
/// @return The project ID.
|
|
20
|
-
function PROJECT_ID() external view returns (uint256);
|
|
21
|
-
}
|