@ballkidz/defifa 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 +2 -0
- package/CRYPTO_ECON.md +955 -0
- package/CRYPTO_ECON.pdf +0 -0
- package/CRYPTO_ECON.tex +800 -0
- package/README.md +119 -0
- package/SKILLS.md +177 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +4867 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +1719 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +1535 -0
- package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +295 -0
- package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +4875 -0
- package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +1725 -0
- package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +1543 -0
- package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +301 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +4875 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +1725 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +1543 -0
- package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +301 -0
- package/deployments/defifa-v5/sepolia/DefifaDelegate.json +4875 -0
- package/deployments/defifa-v5/sepolia/DefifaDeployer.json +1725 -0
- package/deployments/defifa-v5/sepolia/DefifaGovernor.json +1543 -0
- package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +301 -0
- package/foundry.lock +17 -0
- package/foundry.toml +35 -0
- package/package.json +33 -0
- package/remappings.txt +6 -0
- package/script/Deploy.s.sol +109 -0
- package/script/helpers/DefifaDeploymentLib.sol +83 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +521 -0
- package/src/DefifaDeployer.sol +894 -0
- package/src/DefifaGovernor.sol +490 -0
- package/src/DefifaHook.sol +1056 -0
- package/src/DefifaProjectOwner.sol +63 -0
- package/src/DefifaTokenUriResolver.sol +312 -0
- package/src/enums/DefifaGamePhase.sol +11 -0
- package/src/enums/DefifaScorecardState.sol +10 -0
- package/src/interfaces/IDefifaDeployer.sol +108 -0
- package/src/interfaces/IDefifaGamePhaseReporter.sol +8 -0
- package/src/interfaces/IDefifaGamePotReporter.sol +8 -0
- package/src/interfaces/IDefifaGovernor.sol +132 -0
- package/src/interfaces/IDefifaHook.sol +228 -0
- package/src/interfaces/IDefifaTokenUriResolver.sol +10 -0
- package/src/libraries/DefifaFontImporter.sol +19 -0
- package/src/libraries/DefifaHookLib.sol +358 -0
- package/src/structs/DefifaAttestations.sol +9 -0
- package/src/structs/DefifaDelegation.sol +9 -0
- package/src/structs/DefifaLaunchProjectData.sol +59 -0
- package/src/structs/DefifaOpsData.sol +20 -0
- package/src/structs/DefifaScorecard.sol +9 -0
- package/src/structs/DefifaTierCashOutWeight.sol +9 -0
- package/src/structs/DefifaTierParams.sol +16 -0
- package/test/DefifaFeeAccounting.t.sol +559 -0
- package/test/DefifaGovernor.t.sol +1333 -0
- package/test/DefifaMintCostInvariant.t.sol +299 -0
- package/test/DefifaNoContest.t.sol +922 -0
- package/test/DefifaSecurity.t.sol +717 -0
- package/test/SVG.t.sol +164 -0
- package/test/deployScript.t.sol +144 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {DefifaTierParams} from "./DefifaTierParams.sol";
|
|
5
|
+
import {DefifaOpsData} from "./DefifaOpsData.sol";
|
|
6
|
+
|
|
7
|
+
import {JBLaunchProjectConfig} from "@bananapus/721-hook-v6/src/structs/JBLaunchProjectConfig.sol";
|
|
8
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
9
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
10
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
11
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
12
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
13
|
+
|
|
14
|
+
/// @custom:member name The name of the game being created.
|
|
15
|
+
/// @custom:member projectUri Metadata to associate with the project.
|
|
16
|
+
/// @custom:member contractUri The URI to associate with the 721.
|
|
17
|
+
/// @custom:member baseUri The URI base to prepend onto any tier token URIs.
|
|
18
|
+
/// @custom:member tiers Parameters describing the tiers.
|
|
19
|
+
/// @custom:member tierPrice The uniform price for all tiers. All tiers use the same price so that price-based voting
|
|
20
|
+
/// power is equal across tiers.
|
|
21
|
+
/// @custom:member token The token configuration the game is played with.
|
|
22
|
+
/// @custom:member mintPeriodDuration The duration of the game's mint phase, measured in seconds.
|
|
23
|
+
/// @custom:member refundPeriodDuration The time between the mint phase and the start time when mint's are no longer
|
|
24
|
+
/// open but refunds are still allowed, measured in seconds. @custom:member start The time at which the game should
|
|
25
|
+
/// start, measured in seconds.
|
|
26
|
+
/// @custom:member splits Splits to distribute funds between during the game's scoring phase.
|
|
27
|
+
/// @custom:member attestationStartTime The time the attestations will start for all submitted scorecards, measured in
|
|
28
|
+
/// seconds. If in the past, scorecards will start accepting attestations right away. @custom:member
|
|
29
|
+
/// attestationGracePeriod The time period the attestations must be active for once it has started even if it has
|
|
30
|
+
/// already reached quorum, measured in seconds.
|
|
31
|
+
/// @custom:member defaultAttestationDelegate The address that'll be set as the attestation delegate by default.
|
|
32
|
+
/// @custom:member defaultTokenUriResolver The contract used to resolve token URIs if not provided by a tier
|
|
33
|
+
/// specifically. @custom:member terminal The payment terminal where the project will accept funds through.
|
|
34
|
+
/// @custom:member store A contract to store standard JB721 data in.
|
|
35
|
+
/// @custom:member minParticipation The minimum treasury balance required for the game to proceed to scoring. If the
|
|
36
|
+
/// balance is below this when scoring would begin, the game enters NO_CONTEST. Set to 0 to disable. @custom:member
|
|
37
|
+
/// scorecardTimeout The maximum time (in seconds) after the scoring phase begins for a scorecard to be ratified. If
|
|
38
|
+
/// exceeded, the game enters NO_CONTEST. Set to 0 to disable.
|
|
39
|
+
struct DefifaLaunchProjectData {
|
|
40
|
+
string name;
|
|
41
|
+
string projectUri;
|
|
42
|
+
string contractUri;
|
|
43
|
+
string baseUri;
|
|
44
|
+
DefifaTierParams[] tiers;
|
|
45
|
+
uint104 tierPrice;
|
|
46
|
+
JBAccountingContext token;
|
|
47
|
+
uint24 mintPeriodDuration;
|
|
48
|
+
uint24 refundPeriodDuration;
|
|
49
|
+
uint48 start;
|
|
50
|
+
JBSplit[] splits;
|
|
51
|
+
uint256 attestationStartTime;
|
|
52
|
+
uint256 attestationGracePeriod;
|
|
53
|
+
address defaultAttestationDelegate;
|
|
54
|
+
IJB721TokenUriResolver defaultTokenUriResolver;
|
|
55
|
+
IJBTerminal terminal;
|
|
56
|
+
IJB721TiersHookStore store;
|
|
57
|
+
uint256 minParticipation;
|
|
58
|
+
uint32 scorecardTimeout;
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
/// @custom:member token The token being used by the game.
|
|
5
|
+
/// @custom:member start The time at which the game should start, measured in seconds.
|
|
6
|
+
/// @custom:member mintPeriodDuration The duration of the game's mint phase, measured in seconds.
|
|
7
|
+
/// @custom:member refundPeriodDuration The time between the mint phase and the start time when mint's are no longer
|
|
8
|
+
/// open but refunds are still allowed, measured in seconds. @custom:member minParticipation The minimum treasury
|
|
9
|
+
/// balance required for the game to proceed to scoring. If the balance is below this when scoring would begin, the game
|
|
10
|
+
/// enters NO_CONTEST. Set to 0 to disable.
|
|
11
|
+
/// @custom:member scorecardTimeout The maximum time (in seconds) after the scoring phase begins for a scorecard to be
|
|
12
|
+
/// ratified. If exceeded, the game enters NO_CONTEST. Set to 0 to disable.
|
|
13
|
+
struct DefifaOpsData {
|
|
14
|
+
address token;
|
|
15
|
+
uint48 start;
|
|
16
|
+
uint24 mintPeriodDuration;
|
|
17
|
+
uint24 refundPeriodDuration;
|
|
18
|
+
uint256 minParticipation;
|
|
19
|
+
uint32 scorecardTimeout;
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
/// @custom:member attestationsBegin The block at which attestations to the scorecard become allowed.
|
|
5
|
+
/// @custom:member gracePeriodEnds The block at which the scorecard can become ratified.
|
|
6
|
+
struct DefifaScorecard {
|
|
7
|
+
uint48 attestationsBegin;
|
|
8
|
+
uint48 gracePeriodEnds;
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
/// @custom:member id The tier's ID.
|
|
5
|
+
/// @custom:member cashOutWeight The weight that all tokens of this tier can be cashed out for.
|
|
6
|
+
struct DefifaTierCashOutWeight {
|
|
7
|
+
uint256 id;
|
|
8
|
+
uint256 cashOutWeight;
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
/// @custom:member name The name of the tier.
|
|
5
|
+
/// @custom:member reservedRate The number of minted tokens needed in the tier to allow for minting another reserved
|
|
6
|
+
/// token. @custom:member reservedRateBeneficiary The beneficiary of the reserved tokens for this tier.
|
|
7
|
+
/// @custom:member encodedIPFSUri The URI to use for each token within the tier.
|
|
8
|
+
/// @custom:member shouldUseReservedRateBeneficiaryAsDefault A flag indicating if the `reservedTokenBeneficiary` should
|
|
9
|
+
/// be stored as the default beneficiary for all tiers, saving storage.
|
|
10
|
+
struct DefifaTierParams {
|
|
11
|
+
string name;
|
|
12
|
+
uint16 reservedRate;
|
|
13
|
+
address reservedTokenBeneficiary;
|
|
14
|
+
bytes32 encodedIPFSUri;
|
|
15
|
+
bool shouldUseReservedTokenBeneficiaryAsDefault;
|
|
16
|
+
}
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import "../src/DefifaGovernor.sol";
|
|
6
|
+
import "../src/DefifaDeployer.sol";
|
|
7
|
+
import "../src/DefifaHook.sol";
|
|
8
|
+
import "../src/DefifaTokenUriResolver.sol";
|
|
9
|
+
import "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
10
|
+
|
|
11
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
12
|
+
import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
|
|
13
|
+
import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
14
|
+
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
15
|
+
import "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
16
|
+
import "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
17
|
+
import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
18
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
19
|
+
|
|
20
|
+
/// @notice Tests for PR #22 (M-D8): fee accounting after removing duplicate nana fee.
|
|
21
|
+
/// Verifies that only the fee portion of the pot is sent as payouts during fulfillment,
|
|
22
|
+
/// and the remaining balance stays as surplus for cash-outs.
|
|
23
|
+
contract DefifaFeeAccountingTest is JBTest, TestBaseWorkflow {
|
|
24
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
25
|
+
|
|
26
|
+
uint256 _protocolFeeProjectId;
|
|
27
|
+
uint256 _defifaProjectId;
|
|
28
|
+
address projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
29
|
+
|
|
30
|
+
DefifaDeployer deployer;
|
|
31
|
+
DefifaHook hook;
|
|
32
|
+
DefifaGovernor governor;
|
|
33
|
+
uint256 _gameId = 3;
|
|
34
|
+
|
|
35
|
+
function setUp() public virtual override {
|
|
36
|
+
super.setUp();
|
|
37
|
+
|
|
38
|
+
// Terminal configurations.
|
|
39
|
+
JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
|
|
40
|
+
_tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
41
|
+
|
|
42
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
43
|
+
terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
|
|
44
|
+
|
|
45
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
46
|
+
rulesetConfigs[0] = JBRulesetConfig({
|
|
47
|
+
mustStartAtOrAfter: 0,
|
|
48
|
+
duration: 10 days,
|
|
49
|
+
weight: 1e18,
|
|
50
|
+
weightCutPercent: 0,
|
|
51
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
52
|
+
metadata: JBRulesetMetadata({
|
|
53
|
+
reservedPercent: 0,
|
|
54
|
+
cashOutTaxRate: 0,
|
|
55
|
+
baseCurrency: JBCurrencyIds.ETH,
|
|
56
|
+
pausePay: false,
|
|
57
|
+
pauseCreditTransfers: false,
|
|
58
|
+
allowOwnerMinting: false,
|
|
59
|
+
allowSetCustomToken: false,
|
|
60
|
+
allowTerminalMigration: false,
|
|
61
|
+
allowSetTerminals: false,
|
|
62
|
+
allowSetController: false,
|
|
63
|
+
allowAddAccountingContext: false,
|
|
64
|
+
allowAddPriceFeed: false,
|
|
65
|
+
ownerMustSendPayouts: false,
|
|
66
|
+
holdFees: false,
|
|
67
|
+
useTotalSurplusForCashOuts: false,
|
|
68
|
+
useDataHookForPay: true,
|
|
69
|
+
useDataHookForCashOut: true,
|
|
70
|
+
dataHook: address(0),
|
|
71
|
+
metadata: 0
|
|
72
|
+
}),
|
|
73
|
+
splitGroups: new JBSplitGroup[](0),
|
|
74
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Launch the NANA fee project.
|
|
78
|
+
_protocolFeeProjectId =
|
|
79
|
+
jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
80
|
+
vm.prank(projectOwner);
|
|
81
|
+
jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0));
|
|
82
|
+
|
|
83
|
+
// Launch the Defifa fee project.
|
|
84
|
+
_defifaProjectId =
|
|
85
|
+
jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
86
|
+
vm.prank(projectOwner);
|
|
87
|
+
address _defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
88
|
+
|
|
89
|
+
// Look up NANA token.
|
|
90
|
+
address _nanaToken = address(jbTokens().tokenOf(_protocolFeeProjectId));
|
|
91
|
+
|
|
92
|
+
hook = new DefifaHook(jbDirectory(), IERC20(_defifaToken), IERC20(_nanaToken));
|
|
93
|
+
governor = new DefifaGovernor(jbController(), address(this));
|
|
94
|
+
JBAddressRegistry _registry = new JBAddressRegistry();
|
|
95
|
+
DefifaTokenUriResolver _tokenURIResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
|
|
96
|
+
deployer = new DefifaDeployer(
|
|
97
|
+
address(hook),
|
|
98
|
+
_tokenURIResolver,
|
|
99
|
+
governor,
|
|
100
|
+
jbController(),
|
|
101
|
+
_registry,
|
|
102
|
+
_defifaProjectId,
|
|
103
|
+
_protocolFeeProjectId
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Grant the deployer SET_SPLIT_GROUPS permission on the defifa fee project.
|
|
107
|
+
// This is needed so the deployer can set custom splits via controller.setSplitGroupsOf().
|
|
108
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
109
|
+
permissionIds[0] = JBPermissionIds.SET_SPLIT_GROUPS;
|
|
110
|
+
vm.prank(projectOwner);
|
|
111
|
+
jbPermissions()
|
|
112
|
+
.setPermissionsFor(
|
|
113
|
+
projectOwner,
|
|
114
|
+
JBPermissionsData({
|
|
115
|
+
operator: address(deployer), projectId: uint64(_defifaProjectId), permissionIds: permissionIds
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
hook.transferOwnership(address(deployer));
|
|
120
|
+
governor.transferOwnership(address(deployer));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
// Test 1: Fee accounting with default splits (no user splits)
|
|
125
|
+
// defifa = 5%, nana = 2.5%, total commitment = 7.5%
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
function testFeeAccounting_defaultSplits() external {
|
|
128
|
+
uint8 nTiers = 4;
|
|
129
|
+
DefifaLaunchProjectData memory defifaData = _getBasicDefifaLaunchData(nTiers);
|
|
130
|
+
(uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
|
|
131
|
+
|
|
132
|
+
// Mint phase: each user buys 1 NFT for 1 ETH.
|
|
133
|
+
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
134
|
+
address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
|
|
135
|
+
|
|
136
|
+
// Record pot before fulfillment.
|
|
137
|
+
uint256 potBefore =
|
|
138
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
139
|
+
assertEq(potBefore, nTiers * 1 ether, "pot should be nTiers ETH");
|
|
140
|
+
|
|
141
|
+
// Expected fee: pot * (5% + 2.5%) = pot * 7.5%
|
|
142
|
+
// DEFIFA_FEE_DIVISOR = 20 -> 1_000_000_000 / 20 = 50_000_000 (5%)
|
|
143
|
+
// BASE_PROTOCOL_FEE_DIVISOR = 40 -> 1_000_000_000 / 40 = 25_000_000 (2.5%)
|
|
144
|
+
// totalAbsolutePercent = 75_000_000
|
|
145
|
+
uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
146
|
+
uint256 expectedSurplus = potBefore - expectedFee;
|
|
147
|
+
|
|
148
|
+
// Advance through lifecycle and ratify scorecard.
|
|
149
|
+
_ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
|
|
150
|
+
|
|
151
|
+
// Check balance after fulfillment (ratification triggers fulfillment).
|
|
152
|
+
uint256 potAfter =
|
|
153
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
154
|
+
|
|
155
|
+
assertEq(potAfter, expectedSurplus, "surplus after fees should be pot - feeAmount");
|
|
156
|
+
assertEq(
|
|
157
|
+
deployer.fulfilledCommitmentsOf(projectId), expectedFee, "fulfilledCommitmentsOf should equal fee amount"
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Verify currentGamePotOf reporting.
|
|
161
|
+
(uint256 potExcluding,,) = deployer.currentGamePotOf(projectId, false);
|
|
162
|
+
(uint256 potIncluding,,) = deployer.currentGamePotOf(projectId, true);
|
|
163
|
+
assertEq(potExcluding, expectedSurplus, "pot excluding commitments = surplus");
|
|
164
|
+
assertEq(potIncluding, expectedSurplus + expectedFee, "pot including commitments = original pot");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
// Test 2: Cash-out amounts are correct after fee deduction
|
|
169
|
+
// With 4 tiers, even scorecard, each player should get ~(pot - fees) / 4
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
function testCashOutAfterFees() external {
|
|
172
|
+
uint8 nTiers = 4;
|
|
173
|
+
DefifaLaunchProjectData memory defifaData = _getBasicDefifaLaunchData(nTiers);
|
|
174
|
+
(uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
|
|
175
|
+
|
|
176
|
+
// Mint phase.
|
|
177
|
+
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
178
|
+
address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
|
|
179
|
+
|
|
180
|
+
uint256 potBefore =
|
|
181
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
182
|
+
|
|
183
|
+
// Ratify scorecard (triggers fulfillment).
|
|
184
|
+
_ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
|
|
185
|
+
vm.warp(block.timestamp + 1);
|
|
186
|
+
|
|
187
|
+
uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
188
|
+
uint256 surplus = potBefore - expectedFee;
|
|
189
|
+
|
|
190
|
+
// Each user cashes out their NFT.
|
|
191
|
+
uint256 totalCashedOut;
|
|
192
|
+
for (uint256 i = 0; i < nTiers; i++) {
|
|
193
|
+
uint256 balBefore = users[i].balance;
|
|
194
|
+
|
|
195
|
+
uint256[] memory cashOutIds = new uint256[](1);
|
|
196
|
+
cashOutIds[0] = _generateTokenId(i + 1, 1);
|
|
197
|
+
bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
|
|
198
|
+
|
|
199
|
+
vm.prank(users[i]);
|
|
200
|
+
JBMultiTerminal(address(jbMultiTerminal()))
|
|
201
|
+
.cashOutTokensOf({
|
|
202
|
+
holder: users[i],
|
|
203
|
+
projectId: projectId,
|
|
204
|
+
cashOutCount: 0,
|
|
205
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
206
|
+
minTokensReclaimed: 0,
|
|
207
|
+
beneficiary: payable(users[i]),
|
|
208
|
+
metadata: cashOutMetadata
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
totalCashedOut += users[i].balance - balBefore;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Total cashed out should approximately equal the post-fee surplus.
|
|
215
|
+
assertApproxEqRel(totalCashedOut, surplus, 0.001 ether, "total cash-out should equal surplus");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
// Test 3: Fee + remaining balance = original pot (no dust lost)
|
|
220
|
+
// -----------------------------------------------------------------------
|
|
221
|
+
function testFeeAccounting_noRoundingLoss() external {
|
|
222
|
+
uint8 nTiers = 4;
|
|
223
|
+
DefifaLaunchProjectData memory defifaData = _getBasicDefifaLaunchData(nTiers);
|
|
224
|
+
(uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
|
|
225
|
+
|
|
226
|
+
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
227
|
+
address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
|
|
228
|
+
|
|
229
|
+
uint256 potBefore =
|
|
230
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
231
|
+
|
|
232
|
+
_ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
|
|
233
|
+
|
|
234
|
+
uint256 potAfter =
|
|
235
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
236
|
+
|
|
237
|
+
uint256 feeAmount = deployer.fulfilledCommitmentsOf(projectId);
|
|
238
|
+
assertEq(feeAmount + potAfter, potBefore, "fee + surplus should equal original pot exactly");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// -----------------------------------------------------------------------
|
|
242
|
+
// Test 4: Fee accounting with user-provided custom splits
|
|
243
|
+
// User adds a 10% split -> total commitment = 5% + 5% + 10% = 20%
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
function testFeeAccounting_withUserSplits() external {
|
|
246
|
+
uint8 nTiers = 4;
|
|
247
|
+
|
|
248
|
+
JBSplit[] memory customSplits = new JBSplit[](1);
|
|
249
|
+
address splitBeneficiary = address(bytes20(keccak256("charity")));
|
|
250
|
+
customSplits[0] = JBSplit({
|
|
251
|
+
preferAddToBalance: false,
|
|
252
|
+
percent: JBConstants.SPLITS_TOTAL_PERCENT / 10, // 10% = 100_000_000
|
|
253
|
+
projectId: 0,
|
|
254
|
+
beneficiary: payable(splitBeneficiary),
|
|
255
|
+
lockedUntil: 0,
|
|
256
|
+
hook: IJBSplitHook(address(0))
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
DefifaLaunchProjectData memory defifaData = _getDefifaLaunchDataWithSplits(nTiers, customSplits);
|
|
260
|
+
(uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
|
|
261
|
+
|
|
262
|
+
// Mint phase.
|
|
263
|
+
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
264
|
+
address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
|
|
265
|
+
|
|
266
|
+
uint256 potBefore =
|
|
267
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
268
|
+
|
|
269
|
+
// totalAbsolutePercent = 25_000_000 + 50_000_000 + 100_000_000 = 175_000_000 (17.5%)
|
|
270
|
+
uint256 expectedFee = (potBefore * 175_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
271
|
+
uint256 expectedSurplus = potBefore - expectedFee;
|
|
272
|
+
|
|
273
|
+
// Ratify scorecard (triggers fulfillment).
|
|
274
|
+
_ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
|
|
275
|
+
|
|
276
|
+
uint256 potAfter =
|
|
277
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
278
|
+
|
|
279
|
+
assertEq(potAfter, expectedSurplus, "surplus with user splits should be pot - 20%");
|
|
280
|
+
assertEq(deployer.fulfilledCommitmentsOf(projectId), expectedFee, "fulfilled = fee amount with user splits");
|
|
281
|
+
|
|
282
|
+
// Verify beneficiary received funds.
|
|
283
|
+
assertTrue(splitBeneficiary.balance > 0, "split beneficiary should have received funds");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// -----------------------------------------------------------------------
|
|
287
|
+
// Test 5: Cash-out with user splits — surplus is reduced by user split portion
|
|
288
|
+
// -----------------------------------------------------------------------
|
|
289
|
+
function testCashOutAfterFees_withUserSplits() external {
|
|
290
|
+
uint8 nTiers = 4;
|
|
291
|
+
|
|
292
|
+
JBSplit[] memory customSplits = new JBSplit[](1);
|
|
293
|
+
customSplits[0] = JBSplit({
|
|
294
|
+
preferAddToBalance: false,
|
|
295
|
+
percent: JBConstants.SPLITS_TOTAL_PERCENT / 10, // 10%
|
|
296
|
+
projectId: 0,
|
|
297
|
+
beneficiary: payable(address(bytes20(keccak256("charity")))),
|
|
298
|
+
lockedUntil: 0,
|
|
299
|
+
hook: IJBSplitHook(address(0))
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
DefifaLaunchProjectData memory defifaData = _getDefifaLaunchDataWithSplits(nTiers, customSplits);
|
|
303
|
+
(uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
|
|
304
|
+
|
|
305
|
+
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
306
|
+
address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
|
|
307
|
+
|
|
308
|
+
uint256 potBefore =
|
|
309
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
310
|
+
|
|
311
|
+
_ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
|
|
312
|
+
vm.warp(block.timestamp + 1);
|
|
313
|
+
|
|
314
|
+
// 17.5% fee (2.5% nana + 5% defifa + 10% user)
|
|
315
|
+
uint256 expectedFee = (potBefore * 175_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
316
|
+
uint256 surplus = potBefore - expectedFee;
|
|
317
|
+
|
|
318
|
+
// Cash out all tiers.
|
|
319
|
+
uint256 totalCashedOut;
|
|
320
|
+
for (uint256 i = 0; i < nTiers; i++) {
|
|
321
|
+
uint256 balBefore = users[i].balance;
|
|
322
|
+
|
|
323
|
+
uint256[] memory cashOutIds = new uint256[](1);
|
|
324
|
+
cashOutIds[0] = _generateTokenId(i + 1, 1);
|
|
325
|
+
bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
|
|
326
|
+
|
|
327
|
+
vm.prank(users[i]);
|
|
328
|
+
JBMultiTerminal(address(jbMultiTerminal()))
|
|
329
|
+
.cashOutTokensOf({
|
|
330
|
+
holder: users[i],
|
|
331
|
+
projectId: projectId,
|
|
332
|
+
cashOutCount: 0,
|
|
333
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
334
|
+
minTokensReclaimed: 0,
|
|
335
|
+
beneficiary: payable(users[i]),
|
|
336
|
+
metadata: cashOutMetadata
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
totalCashedOut += users[i].balance - balBefore;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
assertApproxEqRel(
|
|
343
|
+
totalCashedOut, surplus, 0.001 ether, "cash-out with user splits should equal surplus after 20% fee"
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Verify that surplus is meaningfully less than with no user splits.
|
|
347
|
+
uint256 surplusWithoutUserSplits = potBefore - (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
348
|
+
assertTrue(surplus < surplusWithoutUserSplits, "user splits should reduce available surplus");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// -----------------------------------------------------------------------
|
|
352
|
+
// Test 6: Splits normalization with awkward percentages
|
|
353
|
+
// -----------------------------------------------------------------------
|
|
354
|
+
function testSplitNormalization_noRoundingLoss() external {
|
|
355
|
+
uint8 nTiers = 4;
|
|
356
|
+
|
|
357
|
+
JBSplit[] memory customSplits = new JBSplit[](3);
|
|
358
|
+
customSplits[0] = JBSplit({
|
|
359
|
+
preferAddToBalance: false,
|
|
360
|
+
percent: 33_333_333, // ~3.33%
|
|
361
|
+
projectId: 0,
|
|
362
|
+
beneficiary: payable(address(0x1111)),
|
|
363
|
+
lockedUntil: 0,
|
|
364
|
+
hook: IJBSplitHook(address(0))
|
|
365
|
+
});
|
|
366
|
+
customSplits[1] = JBSplit({
|
|
367
|
+
preferAddToBalance: false,
|
|
368
|
+
percent: 66_666_666, // ~6.67%
|
|
369
|
+
projectId: 0,
|
|
370
|
+
beneficiary: payable(address(0x2222)),
|
|
371
|
+
lockedUntil: 0,
|
|
372
|
+
hook: IJBSplitHook(address(0))
|
|
373
|
+
});
|
|
374
|
+
customSplits[2] = JBSplit({
|
|
375
|
+
preferAddToBalance: false,
|
|
376
|
+
percent: 11_111_111, // ~1.11%
|
|
377
|
+
projectId: 0,
|
|
378
|
+
beneficiary: payable(address(0x3333)),
|
|
379
|
+
lockedUntil: 0,
|
|
380
|
+
hook: IJBSplitHook(address(0))
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
DefifaLaunchProjectData memory defifaData = _getDefifaLaunchDataWithSplits(nTiers, customSplits);
|
|
384
|
+
(uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
|
|
385
|
+
|
|
386
|
+
vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
|
|
387
|
+
address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
|
|
388
|
+
|
|
389
|
+
uint256 potBefore =
|
|
390
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
391
|
+
|
|
392
|
+
// Ratification should succeed (proves splits normalization works).
|
|
393
|
+
_ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
|
|
394
|
+
|
|
395
|
+
uint256 potAfter =
|
|
396
|
+
jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
397
|
+
|
|
398
|
+
// fee + remaining = original (no dust lost).
|
|
399
|
+
uint256 feeAmount = deployer.fulfilledCommitmentsOf(projectId);
|
|
400
|
+
assertEq(feeAmount + potAfter, potBefore, "fee + surplus should equal original pot exactly");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ========================== Helpers ==========================
|
|
404
|
+
|
|
405
|
+
function _getBasicDefifaLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
|
|
406
|
+
return _getDefifaLaunchDataWithSplits(nTiers, new JBSplit[](0));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function _getDefifaLaunchDataWithSplits(
|
|
410
|
+
uint8 nTiers,
|
|
411
|
+
JBSplit[] memory splits
|
|
412
|
+
)
|
|
413
|
+
internal
|
|
414
|
+
returns (DefifaLaunchProjectData memory)
|
|
415
|
+
{
|
|
416
|
+
DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
|
|
417
|
+
for (uint256 i = 0; i < nTiers; i++) {
|
|
418
|
+
tierParams[i] = DefifaTierParams({
|
|
419
|
+
reservedRate: 1001,
|
|
420
|
+
reservedTokenBeneficiary: address(0),
|
|
421
|
+
encodedIPFSUri: bytes32(0),
|
|
422
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
423
|
+
name: "DEFIFA"
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return DefifaLaunchProjectData({
|
|
428
|
+
name: "DEFIFA",
|
|
429
|
+
projectUri: "",
|
|
430
|
+
contractUri: "",
|
|
431
|
+
baseUri: "",
|
|
432
|
+
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
433
|
+
mintPeriodDuration: 1 days,
|
|
434
|
+
start: uint48(block.timestamp + 3 days),
|
|
435
|
+
refundPeriodDuration: 1 days,
|
|
436
|
+
store: new JB721TiersHookStore(),
|
|
437
|
+
splits: splits,
|
|
438
|
+
attestationStartTime: 0,
|
|
439
|
+
attestationGracePeriod: 100_381,
|
|
440
|
+
defaultAttestationDelegate: address(0),
|
|
441
|
+
tierPrice: uint104(1 ether),
|
|
442
|
+
tiers: tierParams,
|
|
443
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
444
|
+
terminal: jbMultiTerminal(),
|
|
445
|
+
minParticipation: 0,
|
|
446
|
+
scorecardTimeout: 0
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function _createDefifaProject(DefifaLaunchProjectData memory defifaLaunchData)
|
|
451
|
+
internal
|
|
452
|
+
returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
|
|
453
|
+
{
|
|
454
|
+
_governor = governor;
|
|
455
|
+
projectId = deployer.launchGameWith(defifaLaunchData);
|
|
456
|
+
JBRuleset memory _fc = jbRulesets().currentOf(projectId);
|
|
457
|
+
if (_fc.dataHook() == address(0)) {
|
|
458
|
+
(_fc,) = jbRulesets().latestQueuedOf(projectId);
|
|
459
|
+
}
|
|
460
|
+
nft = DefifaHook(_fc.dataHook());
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// @notice Mint 1 NFT per tier, set delegation, return array of user addresses.
|
|
464
|
+
function _mintAllTiers(
|
|
465
|
+
DefifaHook _nft,
|
|
466
|
+
DefifaGovernor _governor,
|
|
467
|
+
uint256 projectId,
|
|
468
|
+
uint8 nTiers
|
|
469
|
+
)
|
|
470
|
+
internal
|
|
471
|
+
returns (address[] memory users)
|
|
472
|
+
{
|
|
473
|
+
users = new address[](nTiers);
|
|
474
|
+
for (uint256 i = 0; i < nTiers; i++) {
|
|
475
|
+
users[i] = address(bytes20(keccak256(abi.encode("feeUser", i))));
|
|
476
|
+
vm.deal(users[i], 1 ether);
|
|
477
|
+
|
|
478
|
+
uint16[] memory rawMetadata = new uint16[](1);
|
|
479
|
+
rawMetadata[0] = uint16(i + 1);
|
|
480
|
+
bytes memory metadata = _buildPayMetadata(abi.encode(users[i], rawMetadata));
|
|
481
|
+
|
|
482
|
+
vm.prank(users[i]);
|
|
483
|
+
jbMultiTerminal().pay{value: 1 ether}(
|
|
484
|
+
projectId, JBConstants.NATIVE_TOKEN, 1 ether, users[i], 0, "", metadata
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Set delegation.
|
|
488
|
+
DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
|
|
489
|
+
delegations[0] = DefifaDelegation({delegatee: users[i], tierId: i + 1});
|
|
490
|
+
vm.prank(users[i]);
|
|
491
|
+
_nft.setTierDelegatesTo(delegations);
|
|
492
|
+
|
|
493
|
+
vm.warp(block.timestamp + 1);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// @notice Submit and ratify an even scorecard (equal weight per tier).
|
|
498
|
+
function _ratifyEvenScorecard(
|
|
499
|
+
address[] memory users,
|
|
500
|
+
DefifaHook _nft,
|
|
501
|
+
DefifaGovernor _governor,
|
|
502
|
+
uint256 projectId,
|
|
503
|
+
uint8 nTiers
|
|
504
|
+
)
|
|
505
|
+
internal
|
|
506
|
+
{
|
|
507
|
+
// Advance to SCORING phase.
|
|
508
|
+
vm.warp(block.timestamp + 2 days);
|
|
509
|
+
|
|
510
|
+
uint256 totalCashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
|
|
511
|
+
|
|
512
|
+
// Build even scorecard.
|
|
513
|
+
DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
|
|
514
|
+
uint256 assigned;
|
|
515
|
+
for (uint256 i = 0; i < nTiers; i++) {
|
|
516
|
+
scorecards[i].id = i + 1;
|
|
517
|
+
scorecards[i].cashOutWeight = totalCashOutWeight / nTiers;
|
|
518
|
+
assigned += scorecards[i].cashOutWeight;
|
|
519
|
+
}
|
|
520
|
+
// Absorb rounding remainder into last tier.
|
|
521
|
+
scorecards[nTiers - 1].cashOutWeight += totalCashOutWeight - assigned;
|
|
522
|
+
|
|
523
|
+
// Submit scorecard.
|
|
524
|
+
uint256 proposalId = _governor.submitScorecardFor(_gameId, scorecards);
|
|
525
|
+
|
|
526
|
+
// Advance to attestation period and vote.
|
|
527
|
+
vm.warp(block.timestamp + _governor.attestationStartTimeOf(_gameId) + 1);
|
|
528
|
+
for (uint256 i = 0; i < users.length; i++) {
|
|
529
|
+
vm.prank(users[i]);
|
|
530
|
+
_governor.attestToScorecardFrom(_gameId, proposalId);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Advance past grace period.
|
|
534
|
+
vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId) + 1);
|
|
535
|
+
|
|
536
|
+
// Ratify (this calls fulfillCommitmentsOf internally).
|
|
537
|
+
_governor.ratifyScorecardFrom(_gameId, scorecards);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
|
|
541
|
+
return (_tierId * 1_000_000_000) + _tokenNumber;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function _buildPayMetadata(bytes memory metadata) internal returns (bytes memory) {
|
|
545
|
+
bytes[] memory data = new bytes[](1);
|
|
546
|
+
data[0] = metadata;
|
|
547
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
548
|
+
ids[0] = metadataHelper().getId("pay", address(hook));
|
|
549
|
+
return metadataHelper().createMetadata(ids, data);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function _buildCashOutMetadata(bytes memory metadata) internal returns (bytes memory) {
|
|
553
|
+
bytes[] memory data = new bytes[](1);
|
|
554
|
+
data[0] = metadata;
|
|
555
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
556
|
+
ids[0] = metadataHelper().getId("cashOut", address(hook));
|
|
557
|
+
return metadataHelper().createMetadata(ids, data);
|
|
558
|
+
}
|
|
559
|
+
}
|