@ballkidz/defifa 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +60 -5
- package/CRYPTO_ECON.md +505 -270
- package/CRYPTO_ECON.pdf +0 -0
- package/CRYPTO_ECON.tex +438 -241
- package/RISKS.md +9 -1
- package/SKILLS.md +3 -2
- package/package.json +6 -6
- package/src/DefifaDeployer.sol +128 -130
- package/src/DefifaGovernor.sol +278 -83
- package/src/DefifaHook.sol +158 -171
- package/src/enums/DefifaScorecardState.sol +1 -0
- package/src/interfaces/IDefifaGovernor.sol +41 -2
- package/src/libraries/DefifaHookLib.sol +69 -62
- package/src/structs/DefifaAttestations.sol +3 -3
- package/src/structs/DefifaLaunchProjectData.sol +1 -0
- package/src/structs/DefifaScorecard.sol +2 -0
- package/test/BWAFunctionComparison.t.sol +1320 -0
- package/test/DefifaAdversarialQuorum.t.sol +52 -37
- package/test/DefifaAuditLowGuards.t.sol +9 -5
- package/test/DefifaFeeAccounting.t.sol +2 -1
- package/test/DefifaGovernanceHardening.t.sol +1311 -0
- package/test/DefifaGovernor.t.sol +4 -2
- package/test/DefifaHookRegressions.t.sol +2 -1
- package/test/DefifaMintCostInvariant.t.sol +2 -1
- package/test/DefifaNoContest.t.sol +3 -2
- package/test/DefifaSecurity.t.sol +54 -41
- package/test/DefifaUSDC.t.sol +3 -2
- package/test/Fork.t.sol +11 -12
- package/test/TestAuditGaps.sol +6 -4
- package/test/TestQALastMile.t.sol +4 -2
- package/test/audit/{CodexAttestationDoubleCount.t.sol → AttestationDoubleCount.t.sol} +3 -2
- package/test/audit/FixPendingReserveDilution.t.sol +366 -0
- package/test/audit/PendingReserveDilution.t.sol +298 -0
- package/test/audit/PendingReserveQuorumGrief.t.sol +355 -0
- package/test/regression/AttestationDelegateBeneficiary.t.sol +2 -1
- package/test/regression/FulfillmentBlocksRatification.t.sol +2 -1
- package/test/regression/GracePeriodBypass.t.sol +2 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
5
|
+
|
|
6
|
+
import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
|
|
7
|
+
import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
|
|
8
|
+
import {DefifaHook} from "../../src/DefifaHook.sol";
|
|
9
|
+
import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
|
|
10
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
11
|
+
import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
|
|
12
|
+
|
|
13
|
+
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
14
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
15
|
+
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
16
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
17
|
+
|
|
18
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
19
|
+
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
20
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
21
|
+
import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
|
|
22
|
+
import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
|
|
23
|
+
import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
|
|
24
|
+
import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
|
|
25
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
26
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
28
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
29
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
30
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
31
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
32
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
33
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
34
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
35
|
+
|
|
36
|
+
/// @notice Verifies the fix for H-2: Pending reserve NFTs are now included in the cash-out weight
|
|
37
|
+
/// denominator. Before the fix, paid holders could cash out before reserves were minted and extract
|
|
38
|
+
/// more than their fair share.
|
|
39
|
+
///
|
|
40
|
+
/// With BWA + HHI-adjusted quorum, a single-tier winner-take-all scorecard gives the sole beneficiary
|
|
41
|
+
/// 0 attestation power (BWA multiplier = 1 - 1 = 0). To allow ratification, we add 3 disinterested
|
|
42
|
+
/// tiers (weight = 0) whose attestors provide governance power to meet the adjusted quorum.
|
|
43
|
+
contract FixPendingReserveDilutionTest is JBTest, TestBaseWorkflow {
|
|
44
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
45
|
+
|
|
46
|
+
address _protocolFeeProjectTokenAccount;
|
|
47
|
+
address _defifaProjectTokenAccount;
|
|
48
|
+
uint256 _protocolFeeProjectId;
|
|
49
|
+
uint256 _defifaProjectId;
|
|
50
|
+
uint256 _gameId = 3;
|
|
51
|
+
|
|
52
|
+
DefifaDeployer deployer;
|
|
53
|
+
DefifaHook hook;
|
|
54
|
+
DefifaGovernor governor;
|
|
55
|
+
|
|
56
|
+
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
57
|
+
address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
|
|
58
|
+
address player = address(bytes20(keccak256("player")));
|
|
59
|
+
address disinterested1 = address(bytes20(keccak256("disinterested1")));
|
|
60
|
+
address disinterested2 = address(bytes20(keccak256("disinterested2")));
|
|
61
|
+
address disinterested3 = address(bytes20(keccak256("disinterested3")));
|
|
62
|
+
|
|
63
|
+
uint256 _pid;
|
|
64
|
+
DefifaHook _nft;
|
|
65
|
+
DefifaGovernor _gov;
|
|
66
|
+
|
|
67
|
+
function setUp() public virtual override {
|
|
68
|
+
super.setUp();
|
|
69
|
+
|
|
70
|
+
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
71
|
+
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
72
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
73
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
74
|
+
JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
|
|
75
|
+
rc[0] = JBRulesetConfig({
|
|
76
|
+
mustStartAtOrAfter: 0,
|
|
77
|
+
duration: 10 days,
|
|
78
|
+
weight: 1e18,
|
|
79
|
+
weightCutPercent: 0,
|
|
80
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
81
|
+
metadata: JBRulesetMetadata({
|
|
82
|
+
reservedPercent: 0,
|
|
83
|
+
cashOutTaxRate: 0,
|
|
84
|
+
baseCurrency: JBCurrencyIds.ETH,
|
|
85
|
+
pausePay: false,
|
|
86
|
+
pauseCreditTransfers: false,
|
|
87
|
+
allowOwnerMinting: false,
|
|
88
|
+
allowSetCustomToken: false,
|
|
89
|
+
allowTerminalMigration: false,
|
|
90
|
+
allowSetTerminals: false,
|
|
91
|
+
allowSetController: false,
|
|
92
|
+
allowAddAccountingContext: false,
|
|
93
|
+
allowAddPriceFeed: false,
|
|
94
|
+
ownerMustSendPayouts: false,
|
|
95
|
+
holdFees: false,
|
|
96
|
+
useTotalSurplusForCashOuts: false,
|
|
97
|
+
useDataHookForPay: true,
|
|
98
|
+
useDataHookForCashOut: true,
|
|
99
|
+
dataHook: address(0),
|
|
100
|
+
metadata: 0
|
|
101
|
+
}),
|
|
102
|
+
splitGroups: new JBSplitGroup[](0),
|
|
103
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
_protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
107
|
+
vm.prank(projectOwner);
|
|
108
|
+
_protocolFeeProjectTokenAccount =
|
|
109
|
+
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
110
|
+
_defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
111
|
+
vm.prank(projectOwner);
|
|
112
|
+
_defifaProjectTokenAccount =
|
|
113
|
+
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
114
|
+
|
|
115
|
+
hook =
|
|
116
|
+
new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
|
|
117
|
+
governor = new DefifaGovernor(jbController(), address(this));
|
|
118
|
+
deployer = new DefifaDeployer(
|
|
119
|
+
address(hook),
|
|
120
|
+
new DefifaTokenUriResolver(ITypeface(address(0))),
|
|
121
|
+
governor,
|
|
122
|
+
jbController(),
|
|
123
|
+
new JBAddressRegistry(),
|
|
124
|
+
_protocolFeeProjectId,
|
|
125
|
+
_defifaProjectId
|
|
126
|
+
);
|
|
127
|
+
hook.transferOwnership(address(deployer));
|
|
128
|
+
governor.transferOwnership(address(deployer));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// @notice With the fix, a paid holder's cash-out share is diluted by pending reserves.
|
|
132
|
+
/// The paid holder should NOT be able to reclaim the full post-fee surplus when pending
|
|
133
|
+
/// reserves exist -- the reserve holder's share must be protected.
|
|
134
|
+
function test_paidHolderCashOutDilutedByPendingReserves() external {
|
|
135
|
+
(_pid, _nft, _gov) = _launch(_launchData());
|
|
136
|
+
|
|
137
|
+
// Mint phase: player mints 1 NFT into tier 1, disinterested users mint tiers 2-4.
|
|
138
|
+
vm.warp(block.timestamp + 1 days + 1);
|
|
139
|
+
_mint(player, 1, 1 ether);
|
|
140
|
+
_delegateSelf(player, 1);
|
|
141
|
+
vm.warp(block.timestamp + 1);
|
|
142
|
+
_mint(disinterested1, 2, 1 ether);
|
|
143
|
+
_delegateSelf(disinterested1, 2);
|
|
144
|
+
vm.warp(block.timestamp + 1);
|
|
145
|
+
_mint(disinterested2, 3, 1 ether);
|
|
146
|
+
_delegateSelf(disinterested2, 3);
|
|
147
|
+
vm.warp(block.timestamp + 1);
|
|
148
|
+
_mint(disinterested3, 4, 1 ether);
|
|
149
|
+
_delegateSelf(disinterested3, 4);
|
|
150
|
+
|
|
151
|
+
// Verify there is a pending reserve for tier 1.
|
|
152
|
+
assertEq(_nft.store().numberOfPendingReservesFor(address(_nft), 1), 1, "one reserve should be pending");
|
|
153
|
+
|
|
154
|
+
// Advance to scoring phase.
|
|
155
|
+
vm.warp(block.timestamp + 2 days + 1);
|
|
156
|
+
|
|
157
|
+
// Submit scorecard giving all weight to tier 1; tiers 2-4 get 0 (disinterested attestors).
|
|
158
|
+
DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
|
|
159
|
+
sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
|
|
160
|
+
sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
|
|
161
|
+
sc[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
|
|
162
|
+
sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
|
|
163
|
+
uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
|
|
164
|
+
|
|
165
|
+
// Disinterested users attest (they have full BWA power since their tiers get 0 weight).
|
|
166
|
+
// The player (tier 1, 100% weight) has 0 BWA power and cannot meaningfully attest.
|
|
167
|
+
vm.prank(disinterested1);
|
|
168
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
169
|
+
vm.prank(disinterested2);
|
|
170
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
171
|
+
vm.prank(disinterested3);
|
|
172
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
173
|
+
|
|
174
|
+
vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
|
|
175
|
+
_gov.ratifyScorecardFrom(_gameId, sc);
|
|
176
|
+
|
|
177
|
+
// The player should only reclaim HALF of their tier's share of the post-fee surplus
|
|
178
|
+
// (1 of 2 tokens in tier 1, since the pending reserve counts in the denominator).
|
|
179
|
+
// Note: total pot includes 4 ETH from all minters but tier 1 gets 100% of weight.
|
|
180
|
+
// Fees are taken from the terminal surplus. Post-fee surplus is available for cash-out.
|
|
181
|
+
uint256 postFeeSurplus = 4 ether - (4 ether / 20) - (4 ether / 40);
|
|
182
|
+
// Tier 1 gets 100% weight. Player holds 1 of 2 units (1 paid + 1 pending reserve).
|
|
183
|
+
uint256 expectedPlayerReclaim = postFeeSurplus / 2;
|
|
184
|
+
|
|
185
|
+
uint256 beforePlayerBalance = player.balance;
|
|
186
|
+
_cashOut(player, 1, 1);
|
|
187
|
+
uint256 playerReclaim = player.balance - beforePlayerBalance;
|
|
188
|
+
|
|
189
|
+
// The player should receive approximately half the surplus, not the full amount.
|
|
190
|
+
assertApproxEqAbs(
|
|
191
|
+
playerReclaim,
|
|
192
|
+
expectedPlayerReclaim,
|
|
193
|
+
1, // 1 wei tolerance for rounding
|
|
194
|
+
"paid holder should only reclaim half due to pending reserve dilution"
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Specifically, the player should NOT get the full pot.
|
|
198
|
+
assertLt(playerReclaim, postFeeSurplus, "paid holder should NOT reclaim full surplus with pending reserves");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// @notice After reserves are minted, the reserve holder should be able to cash out their share.
|
|
202
|
+
function test_reserveHolderCanCashOutAfterMinting() external {
|
|
203
|
+
(_pid, _nft, _gov) = _launch(_launchData());
|
|
204
|
+
|
|
205
|
+
// Mint phase: player mints 1 NFT into tier 1, disinterested users mint tiers 2-4.
|
|
206
|
+
vm.warp(block.timestamp + 1 days + 1);
|
|
207
|
+
_mint(player, 1, 1 ether);
|
|
208
|
+
_delegateSelf(player, 1);
|
|
209
|
+
vm.warp(block.timestamp + 1);
|
|
210
|
+
_mint(disinterested1, 2, 1 ether);
|
|
211
|
+
_delegateSelf(disinterested1, 2);
|
|
212
|
+
vm.warp(block.timestamp + 1);
|
|
213
|
+
_mint(disinterested2, 3, 1 ether);
|
|
214
|
+
_delegateSelf(disinterested2, 3);
|
|
215
|
+
vm.warp(block.timestamp + 1);
|
|
216
|
+
_mint(disinterested3, 4, 1 ether);
|
|
217
|
+
_delegateSelf(disinterested3, 4);
|
|
218
|
+
|
|
219
|
+
// Advance to scoring phase.
|
|
220
|
+
vm.warp(block.timestamp + 2 days + 1);
|
|
221
|
+
|
|
222
|
+
// Submit scorecard: tier 1 gets all weight; tiers 2-4 are disinterested.
|
|
223
|
+
DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
|
|
224
|
+
sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
|
|
225
|
+
sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
|
|
226
|
+
sc[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
|
|
227
|
+
sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
|
|
228
|
+
uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
|
|
229
|
+
|
|
230
|
+
// Disinterested users attest (full BWA power since 0 weight tiers).
|
|
231
|
+
vm.prank(disinterested1);
|
|
232
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
233
|
+
vm.prank(disinterested2);
|
|
234
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
235
|
+
vm.prank(disinterested3);
|
|
236
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
237
|
+
|
|
238
|
+
vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
|
|
239
|
+
_gov.ratifyScorecardFrom(_gameId, sc);
|
|
240
|
+
|
|
241
|
+
// Mint the reserve NFTs for tier 1.
|
|
242
|
+
JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
|
|
243
|
+
reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
|
|
244
|
+
_nft.mintReservesFor(reserveConfigs);
|
|
245
|
+
assertEq(_nft.balanceOf(reserveBeneficiary), 1, "reserve NFT should be minted");
|
|
246
|
+
|
|
247
|
+
// Now player cashes out.
|
|
248
|
+
uint256 beforePlayerBalance = player.balance;
|
|
249
|
+
_cashOut(player, 1, 1);
|
|
250
|
+
uint256 playerReclaim = player.balance - beforePlayerBalance;
|
|
251
|
+
|
|
252
|
+
// Reserve holder cashes out (token ID for tier 1, token number 2).
|
|
253
|
+
uint256 beforeReserveBalance = reserveBeneficiary.balance;
|
|
254
|
+
_cashOut(reserveBeneficiary, 1, 2);
|
|
255
|
+
uint256 reserveReclaim = reserveBeneficiary.balance - beforeReserveBalance;
|
|
256
|
+
|
|
257
|
+
// Both should get approximately equal shares.
|
|
258
|
+
assertApproxEqAbs(
|
|
259
|
+
playerReclaim,
|
|
260
|
+
reserveReclaim,
|
|
261
|
+
1, // 1 wei tolerance
|
|
262
|
+
"paid and reserve holders should get equal shares"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- helpers ----
|
|
267
|
+
|
|
268
|
+
function _launchData() internal returns (DefifaLaunchProjectData memory) {
|
|
269
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](4);
|
|
270
|
+
// Tier 1: has reserves (the tier under test)
|
|
271
|
+
tp[0] = DefifaTierParams({
|
|
272
|
+
reservedRate: 1, // 1 reserve per mint
|
|
273
|
+
reservedTokenBeneficiary: reserveBeneficiary,
|
|
274
|
+
encodedIPFSUri: bytes32(0),
|
|
275
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
276
|
+
name: "TEAM"
|
|
277
|
+
});
|
|
278
|
+
// Tiers 2-4: disinterested attestors (no reserves, standard rate)
|
|
279
|
+
for (uint256 i = 1; i < 4; i++) {
|
|
280
|
+
tp[i] = DefifaTierParams({
|
|
281
|
+
reservedRate: 1001,
|
|
282
|
+
reservedTokenBeneficiary: address(0),
|
|
283
|
+
encodedIPFSUri: bytes32(0),
|
|
284
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
285
|
+
name: "TEAM"
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return DefifaLaunchProjectData({
|
|
290
|
+
name: "DEFIFA",
|
|
291
|
+
projectUri: "",
|
|
292
|
+
contractUri: "",
|
|
293
|
+
baseUri: "",
|
|
294
|
+
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
295
|
+
mintPeriodDuration: 1 days,
|
|
296
|
+
start: uint48(block.timestamp + 3 days),
|
|
297
|
+
refundPeriodDuration: 1 days,
|
|
298
|
+
store: new JB721TiersHookStore(),
|
|
299
|
+
splits: new JBSplit[](0),
|
|
300
|
+
attestationStartTime: 0,
|
|
301
|
+
attestationGracePeriod: 100_381,
|
|
302
|
+
defaultAttestationDelegate: address(0),
|
|
303
|
+
tierPrice: 1 ether,
|
|
304
|
+
tiers: tp,
|
|
305
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
306
|
+
terminal: jbMultiTerminal(),
|
|
307
|
+
minParticipation: 0,
|
|
308
|
+
scorecardTimeout: 0,
|
|
309
|
+
timelockDuration: 0
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
|
|
314
|
+
g = governor;
|
|
315
|
+
p = deployer.launchGameWith(d);
|
|
316
|
+
JBRuleset memory fc = jbRulesets().currentOf(p);
|
|
317
|
+
if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
|
|
318
|
+
n = DefifaHook(fc.dataHook());
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function _mint(address user, uint256 tid, uint256 amt) internal {
|
|
322
|
+
vm.deal(user, amt);
|
|
323
|
+
uint16[] memory m = new uint16[](1);
|
|
324
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
325
|
+
m[0] = uint16(tid);
|
|
326
|
+
bytes[] memory data = new bytes[](1);
|
|
327
|
+
data[0] = abi.encode(user, m);
|
|
328
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
329
|
+
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
330
|
+
bytes memory metadata = metadataHelper().createMetadata(ids, data);
|
|
331
|
+
vm.prank(user);
|
|
332
|
+
jbMultiTerminal().pay{value: amt}(_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadata);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function _delegateSelf(address user, uint256 tid) internal {
|
|
336
|
+
DefifaDelegation[] memory dd = new DefifaDelegation[](1);
|
|
337
|
+
dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
|
|
338
|
+
vm.prank(user);
|
|
339
|
+
_nft.setTierDelegatesTo(dd);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _cashOut(address user, uint256 tid, uint256 tnum) internal {
|
|
343
|
+
bytes memory meta = _cashOutMeta(tid, tnum);
|
|
344
|
+
vm.prank(user);
|
|
345
|
+
jbMultiTerminal()
|
|
346
|
+
.cashOutTokensOf({
|
|
347
|
+
holder: user,
|
|
348
|
+
projectId: _pid,
|
|
349
|
+
cashOutCount: 0,
|
|
350
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
351
|
+
minTokensReclaimed: 0,
|
|
352
|
+
beneficiary: payable(user),
|
|
353
|
+
metadata: meta
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
|
|
358
|
+
uint256[] memory cid = new uint256[](1);
|
|
359
|
+
cid[0] = (tid * 1_000_000_000) + tnum;
|
|
360
|
+
bytes[] memory data = new bytes[](1);
|
|
361
|
+
data[0] = abi.encode(cid);
|
|
362
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
363
|
+
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
364
|
+
return metadataHelper().createMetadata(ids, data);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
5
|
+
|
|
6
|
+
import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
|
|
7
|
+
import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
|
|
8
|
+
import {DefifaHook} from "../../src/DefifaHook.sol";
|
|
9
|
+
import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
|
|
10
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
11
|
+
import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
|
|
12
|
+
|
|
13
|
+
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
14
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
15
|
+
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
16
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
17
|
+
|
|
18
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
19
|
+
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
20
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
21
|
+
import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
|
|
22
|
+
import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
|
|
23
|
+
import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
|
|
24
|
+
import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
|
|
25
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
26
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
28
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
29
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
30
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
31
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
32
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
33
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
34
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
35
|
+
|
|
36
|
+
contract PendingReserveDilutionTest is JBTest, TestBaseWorkflow {
|
|
37
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
38
|
+
|
|
39
|
+
address _protocolFeeProjectTokenAccount;
|
|
40
|
+
address _defifaProjectTokenAccount;
|
|
41
|
+
uint256 _protocolFeeProjectId;
|
|
42
|
+
uint256 _defifaProjectId;
|
|
43
|
+
uint256 _gameId = 3;
|
|
44
|
+
|
|
45
|
+
DefifaDeployer deployer;
|
|
46
|
+
DefifaHook hook;
|
|
47
|
+
DefifaGovernor governor;
|
|
48
|
+
|
|
49
|
+
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
50
|
+
address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
|
|
51
|
+
address player = address(bytes20(keccak256("player")));
|
|
52
|
+
address disinterested1 = address(bytes20(keccak256("disinterested1")));
|
|
53
|
+
address disinterested2 = address(bytes20(keccak256("disinterested2")));
|
|
54
|
+
address disinterested3 = address(bytes20(keccak256("disinterested3")));
|
|
55
|
+
|
|
56
|
+
uint256 _pid;
|
|
57
|
+
DefifaHook _nft;
|
|
58
|
+
DefifaGovernor _gov;
|
|
59
|
+
|
|
60
|
+
function setUp() public virtual override {
|
|
61
|
+
super.setUp();
|
|
62
|
+
|
|
63
|
+
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
64
|
+
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
65
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
66
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
67
|
+
JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
|
|
68
|
+
rc[0] = JBRulesetConfig({
|
|
69
|
+
mustStartAtOrAfter: 0,
|
|
70
|
+
duration: 10 days,
|
|
71
|
+
weight: 1e18,
|
|
72
|
+
weightCutPercent: 0,
|
|
73
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
74
|
+
metadata: JBRulesetMetadata({
|
|
75
|
+
reservedPercent: 0,
|
|
76
|
+
cashOutTaxRate: 0,
|
|
77
|
+
baseCurrency: JBCurrencyIds.ETH,
|
|
78
|
+
pausePay: false,
|
|
79
|
+
pauseCreditTransfers: false,
|
|
80
|
+
allowOwnerMinting: false,
|
|
81
|
+
allowSetCustomToken: false,
|
|
82
|
+
allowTerminalMigration: false,
|
|
83
|
+
allowSetTerminals: false,
|
|
84
|
+
allowSetController: false,
|
|
85
|
+
allowAddAccountingContext: false,
|
|
86
|
+
allowAddPriceFeed: false,
|
|
87
|
+
ownerMustSendPayouts: false,
|
|
88
|
+
holdFees: false,
|
|
89
|
+
useTotalSurplusForCashOuts: false,
|
|
90
|
+
useDataHookForPay: true,
|
|
91
|
+
useDataHookForCashOut: true,
|
|
92
|
+
dataHook: address(0),
|
|
93
|
+
metadata: 0
|
|
94
|
+
}),
|
|
95
|
+
splitGroups: new JBSplitGroup[](0),
|
|
96
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
_protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
100
|
+
vm.prank(projectOwner);
|
|
101
|
+
_protocolFeeProjectTokenAccount =
|
|
102
|
+
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
103
|
+
_defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
|
|
104
|
+
vm.prank(projectOwner);
|
|
105
|
+
_defifaProjectTokenAccount =
|
|
106
|
+
address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
107
|
+
|
|
108
|
+
hook =
|
|
109
|
+
new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
|
|
110
|
+
governor = new DefifaGovernor(jbController(), address(this));
|
|
111
|
+
deployer = new DefifaDeployer(
|
|
112
|
+
address(hook),
|
|
113
|
+
new DefifaTokenUriResolver(ITypeface(address(0))),
|
|
114
|
+
governor,
|
|
115
|
+
jbController(),
|
|
116
|
+
new JBAddressRegistry(),
|
|
117
|
+
_protocolFeeProjectId,
|
|
118
|
+
_defifaProjectId
|
|
119
|
+
);
|
|
120
|
+
hook.transferOwnership(address(deployer));
|
|
121
|
+
governor.transferOwnership(address(deployer));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// @notice After the H-2 fix, pending reserves dilute the paid holder's cash-out share.
|
|
125
|
+
/// The paid holder can no longer drain the full surplus; reserve holders retain their share.
|
|
126
|
+
///
|
|
127
|
+
/// With BWA + HHI, a single-tier winner-take-all scorecard gives the beneficiary 0 attestation
|
|
128
|
+
/// power. We add 3 disinterested tiers (0 weight) so their attestors can meet the adjusted quorum.
|
|
129
|
+
function test_pendingReserveDilutesPaidHolderCashOut_afterFix() external {
|
|
130
|
+
(_pid, _nft, _gov) = _launch(_launchData());
|
|
131
|
+
|
|
132
|
+
// Mint phase: player mints tier 1, disinterested users mint tiers 2-4.
|
|
133
|
+
vm.warp(block.timestamp + 1 days + 1);
|
|
134
|
+
_mint(player, 1, 1 ether);
|
|
135
|
+
_delegateSelf(player, 1);
|
|
136
|
+
vm.warp(block.timestamp + 1);
|
|
137
|
+
_mint(disinterested1, 2, 1 ether);
|
|
138
|
+
_delegateSelf(disinterested1, 2);
|
|
139
|
+
vm.warp(block.timestamp + 1);
|
|
140
|
+
_mint(disinterested2, 3, 1 ether);
|
|
141
|
+
_delegateSelf(disinterested2, 3);
|
|
142
|
+
vm.warp(block.timestamp + 1);
|
|
143
|
+
_mint(disinterested3, 4, 1 ether);
|
|
144
|
+
_delegateSelf(disinterested3, 4);
|
|
145
|
+
|
|
146
|
+
assertEq(_nft.store().numberOfPendingReservesFor(address(_nft), 1), 1, "one reserve should be pending");
|
|
147
|
+
|
|
148
|
+
vm.warp(block.timestamp + 2 days + 1);
|
|
149
|
+
|
|
150
|
+
// Scorecard: tier 1 gets all weight; tiers 2-4 get 0.
|
|
151
|
+
DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
|
|
152
|
+
sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
|
|
153
|
+
sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
|
|
154
|
+
sc[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
|
|
155
|
+
sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
|
|
156
|
+
uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
|
|
157
|
+
|
|
158
|
+
// Disinterested users attest (full BWA power since 0 weight tiers).
|
|
159
|
+
vm.prank(disinterested1);
|
|
160
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
161
|
+
vm.prank(disinterested2);
|
|
162
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
163
|
+
vm.prank(disinterested3);
|
|
164
|
+
_gov.attestToScorecardFrom(_gameId, proposalId);
|
|
165
|
+
|
|
166
|
+
vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
|
|
167
|
+
_gov.ratifyScorecardFrom(_gameId, sc);
|
|
168
|
+
|
|
169
|
+
// Post-fee surplus from all 4 ETH (4 minters). Tier 1 gets 100% weight.
|
|
170
|
+
uint256 totalPot = 4 ether - (4 ether / 20) - (4 ether / 40);
|
|
171
|
+
// Player holds 1 of 2 units in tier 1 (1 paid + 1 pending reserve in denominator).
|
|
172
|
+
uint256 expectedPlayerReclaim = totalPot / 2;
|
|
173
|
+
|
|
174
|
+
uint256 beforePlayerBalance = player.balance;
|
|
175
|
+
_cashOut(player, 1, 1);
|
|
176
|
+
uint256 playerReclaim = player.balance - beforePlayerBalance;
|
|
177
|
+
|
|
178
|
+
// After the fix: paid holder gets only HALF because pending reserve dilutes the denominator.
|
|
179
|
+
assertApproxEqAbs(
|
|
180
|
+
playerReclaim,
|
|
181
|
+
expectedPlayerReclaim,
|
|
182
|
+
1, // 1 wei tolerance for rounding
|
|
183
|
+
"paid holder reclaims only half due to pending reserve dilution"
|
|
184
|
+
);
|
|
185
|
+
assertLt(playerReclaim, totalPot, "paid holder should NOT reclaim full surplus");
|
|
186
|
+
|
|
187
|
+
// Mint the reserve NFTs.
|
|
188
|
+
JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
|
|
189
|
+
reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
|
|
190
|
+
_nft.mintReservesFor(reserveConfigs);
|
|
191
|
+
assertEq(_nft.balanceOf(reserveBeneficiary), 1, "reserve NFT minted successfully");
|
|
192
|
+
|
|
193
|
+
// Reserve holder should now be able to cash out their share (not zero).
|
|
194
|
+
uint256 beforeReserveBalance = reserveBeneficiary.balance;
|
|
195
|
+
_cashOut(reserveBeneficiary, 1, 2);
|
|
196
|
+
uint256 reserveReclaim = reserveBeneficiary.balance - beforeReserveBalance;
|
|
197
|
+
assertGt(reserveReclaim, 0, "reserve holder can reclaim their share after fix");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _launchData() internal returns (DefifaLaunchProjectData memory) {
|
|
201
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](4);
|
|
202
|
+
// Tier 1: has reserves (the tier under test)
|
|
203
|
+
tp[0] = DefifaTierParams({
|
|
204
|
+
reservedRate: 1,
|
|
205
|
+
reservedTokenBeneficiary: reserveBeneficiary,
|
|
206
|
+
encodedIPFSUri: bytes32(0),
|
|
207
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
208
|
+
name: "TEAM"
|
|
209
|
+
});
|
|
210
|
+
// Tiers 2-4: disinterested attestors (no reserves)
|
|
211
|
+
for (uint256 i = 1; i < 4; i++) {
|
|
212
|
+
tp[i] = DefifaTierParams({
|
|
213
|
+
reservedRate: 1001,
|
|
214
|
+
reservedTokenBeneficiary: address(0),
|
|
215
|
+
encodedIPFSUri: bytes32(0),
|
|
216
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
217
|
+
name: "TEAM"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return DefifaLaunchProjectData({
|
|
222
|
+
name: "DEFIFA",
|
|
223
|
+
projectUri: "",
|
|
224
|
+
contractUri: "",
|
|
225
|
+
baseUri: "",
|
|
226
|
+
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
227
|
+
mintPeriodDuration: 1 days,
|
|
228
|
+
start: uint48(block.timestamp + 3 days),
|
|
229
|
+
refundPeriodDuration: 1 days,
|
|
230
|
+
store: new JB721TiersHookStore(),
|
|
231
|
+
splits: new JBSplit[](0),
|
|
232
|
+
attestationStartTime: 0,
|
|
233
|
+
attestationGracePeriod: 100_381,
|
|
234
|
+
defaultAttestationDelegate: address(0),
|
|
235
|
+
tierPrice: 1 ether,
|
|
236
|
+
tiers: tp,
|
|
237
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
238
|
+
terminal: jbMultiTerminal(),
|
|
239
|
+
minParticipation: 0,
|
|
240
|
+
scorecardTimeout: 0,
|
|
241
|
+
timelockDuration: 0
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
|
|
246
|
+
g = governor;
|
|
247
|
+
p = deployer.launchGameWith(d);
|
|
248
|
+
JBRuleset memory fc = jbRulesets().currentOf(p);
|
|
249
|
+
if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
|
|
250
|
+
n = DefifaHook(fc.dataHook());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _mint(address user, uint256 tid, uint256 amt) internal {
|
|
254
|
+
vm.deal(user, amt);
|
|
255
|
+
uint16[] memory m = new uint16[](1);
|
|
256
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
257
|
+
m[0] = uint16(tid);
|
|
258
|
+
bytes[] memory data = new bytes[](1);
|
|
259
|
+
data[0] = abi.encode(user, m);
|
|
260
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
261
|
+
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
262
|
+
bytes memory metadata = metadataHelper().createMetadata(ids, data);
|
|
263
|
+
vm.prank(user);
|
|
264
|
+
jbMultiTerminal().pay{value: amt}(_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadata);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function _delegateSelf(address user, uint256 tid) internal {
|
|
268
|
+
DefifaDelegation[] memory dd = new DefifaDelegation[](1);
|
|
269
|
+
dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
|
|
270
|
+
vm.prank(user);
|
|
271
|
+
_nft.setTierDelegatesTo(dd);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _cashOut(address user, uint256 tid, uint256 tnum) internal {
|
|
275
|
+
bytes memory meta = _cashOutMeta(tid, tnum);
|
|
276
|
+
vm.prank(user);
|
|
277
|
+
jbMultiTerminal()
|
|
278
|
+
.cashOutTokensOf({
|
|
279
|
+
holder: user,
|
|
280
|
+
projectId: _pid,
|
|
281
|
+
cashOutCount: 0,
|
|
282
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
283
|
+
minTokensReclaimed: 0,
|
|
284
|
+
beneficiary: payable(user),
|
|
285
|
+
metadata: meta
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
|
|
290
|
+
uint256[] memory cid = new uint256[](1);
|
|
291
|
+
cid[0] = (tid * 1_000_000_000) + tnum;
|
|
292
|
+
bytes[] memory data = new bytes[](1);
|
|
293
|
+
data[0] = abi.encode(cid);
|
|
294
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
295
|
+
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
296
|
+
return metadataHelper().createMetadata(ids, data);
|
|
297
|
+
}
|
|
298
|
+
}
|