@ballkidz/defifa 0.0.23 → 0.0.24
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/CHANGELOG.md +4 -0
- package/CRYPTO_ECON.md +2 -2
- package/package.json +1 -1
- package/src/DefifaDeployer.sol +11 -7
- package/src/DefifaHook.sol +28 -0
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +112 -0
- package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +238 -0
- package/test/audit/CurrencyMismatchFix.t.sol +265 -0
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,10 @@ This file instead describes the current v6 repo at a high level and the broad mi
|
|
|
20
20
|
- The v6 surface is split across dedicated deployer, hook, governor, project-owner, and token-uri contracts, with dedicated regression and audit test coverage around governance, fee accounting, attestations, and lifecycle edge cases.
|
|
21
21
|
- Solidity and tooling were upgraded to the v6 baseline around `0.8.28`.
|
|
22
22
|
|
|
23
|
+
## Local audit remediations
|
|
24
|
+
|
|
25
|
+
- Reserve-minted NFTs are now excluded from refund calculations during MINT, REFUND, and NO_CONTEST phases. A public `isReserveMint` mapping tracks which tokens were created via tier reserve frequency rather than paid for. `beforeCashOutRecordedWith` subtracts their tier price from `cumulativeMintPrice`, preventing reserve beneficiaries from withdrawing funds they never contributed.
|
|
26
|
+
|
|
23
27
|
## Migration notes
|
|
24
28
|
|
|
25
29
|
- Do not treat this repo as part of the deployed v5-to-v6 ecosystem delta.
|
package/CRYPTO_ECON.md
CHANGED
|
@@ -231,7 +231,7 @@ $$M(t^+) = M(t^-) - q \cdot p \tag{10}$$
|
|
|
231
231
|
|
|
232
232
|
The refund phase creates a *free option* for participants: they can observe late-breaking information (injury reports, market movements, team changes) and exit at zero cost. This option has value and we analyze its implications in Section 5.2.
|
|
233
233
|
|
|
234
|
-
**Key property.** The refund is dollar-for-dollar: every token refunded removes exactly its mint price from the pot. Because all tiers share the uniform price $p$, the per-NFT backing ratio $B(t) / N_{\text{total}}(t) = p$ is always preserved.
|
|
234
|
+
**Key property.** The refund is dollar-for-dollar: every token refunded removes exactly its mint price from the pot. Because all tiers share the uniform price $p$, the per-NFT backing ratio $B(t) / N_{\text{total}}(t) = p$ is always preserved. Reserve-minted tokens are excluded from refund eligibility — only tokens that were paid for contribute to (and may withdraw from) the pot.
|
|
235
235
|
|
|
236
236
|
**World Cup example.** Two days before the tournament, a star player for Brazil suffers an injury. 300 Brazil holders refund their NFTs, reducing Brazil's count from 1,500 to 1,200 and the pot from 150 ETH to 147 ETH. The refund activity itself signals the belief shift — other participants observe the on-chain refund volume and update their expectations accordingly.
|
|
237
237
|
|
|
@@ -810,7 +810,7 @@ This is the only mechanism that provides a hard, trustless, time-bounded guarant
|
|
|
810
810
|
|
|
811
811
|
The explicit trigger is necessary because the NO_CONTEST phase is initially a *computed* state (the view function returns it based on conditions), but the on-chain ruleset still has the scoring-phase configuration. The trigger queues a new ruleset that enables the actual cash-out mechanics.
|
|
812
812
|
|
|
813
|
-
**Cash-out behavior.** During NO_CONTEST, the `computeCashOutCount` function in `DefifaHookLib` returns `cumulativeMintPrice` — the same amount the player originally paid. This is identical to the MINT/REFUND phase behavior, implementing a complete refund.
|
|
813
|
+
**Cash-out behavior.** During NO_CONTEST, the `computeCashOutCount` function in `DefifaHookLib` returns `cumulativeMintPrice` — the same amount the player originally paid. This is identical to the MINT/REFUND phase behavior, implementing a complete refund. Reserve-minted tokens (those created via tier `reserveFrequency` rather than paid for) are excluded from refund calculations: their `isReserveMint` flag is set at mint time, and `beforeCashOutRecordedWith` subtracts their tier price from `cumulativeMintPrice` during MINT, REFUND, and NO_CONTEST phases. This prevents reserve beneficiaries from draining the pot by cashing out tokens they never paid for.
|
|
814
814
|
|
|
815
815
|
#### 9.1.4 Priority Rules
|
|
816
816
|
|
package/package.json
CHANGED
package/src/DefifaDeployer.sol
CHANGED
|
@@ -53,6 +53,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
|
|
|
53
53
|
error DefifaDeployer_InvalidFeePercent();
|
|
54
54
|
error DefifaDeployer_InvalidGameConfiguration();
|
|
55
55
|
error DefifaDeployer_IncorrectDecimalAmount();
|
|
56
|
+
error DefifaDeployer_InvalidCurrency();
|
|
56
57
|
error DefifaDeployer_NotNoContest();
|
|
57
58
|
error DefifaDeployer_NoContestAlreadyTriggered();
|
|
58
59
|
error DefifaDeployer_TerminalNotFound();
|
|
@@ -337,16 +338,12 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
|
|
|
337
338
|
fulfilledCommitmentsOf[gameId] = feeAmount > 0 ? feeAmount : 1;
|
|
338
339
|
|
|
339
340
|
// Send only the fee portion as payouts. The remaining balance stays as surplus for cash-outs.
|
|
341
|
+
// Use the ruleset's baseCurrency — this matches the currency under which payout limits were stored
|
|
342
|
+
// at launch time, regardless of whether the token is native ETH or an ERC-20.
|
|
340
343
|
// Wrapped in try-catch so the final ruleset is always queued even if payout fails.
|
|
341
344
|
// slither-disable-next-line unused-return,reentrancy-no-eth
|
|
342
345
|
try terminal.sendPayoutsOf({
|
|
343
|
-
projectId: gameId,
|
|
344
|
-
token: token,
|
|
345
|
-
amount: feeAmount,
|
|
346
|
-
// Casting address to uint32 via uint160 is the standard Juicebox token-to-currency conversion.
|
|
347
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
348
|
-
currency: token == JBConstants.NATIVE_TOKEN ? metadata.baseCurrency : uint32(uint160(token)),
|
|
349
|
-
minTokensPaidOut: 0
|
|
346
|
+
projectId: gameId, token: token, amount: feeAmount, currency: metadata.baseCurrency, minTokensPaidOut: 0
|
|
350
347
|
}) {}
|
|
351
348
|
catch (bytes memory reason) {
|
|
352
349
|
// Payout failed — fee stays in pot. Reset to sentinel (1) so currentGamePotOf
|
|
@@ -397,6 +394,13 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
|
|
|
397
394
|
// The hook and governor hardcode uint256[128] tier-weight tables, so reject games with more than 128 tiers.
|
|
398
395
|
if (launchProjectData.tiers.length > 128) revert DefifaDeployer_InvalidGameConfiguration();
|
|
399
396
|
|
|
397
|
+
// Reject ERC-20 games with a zero currency — a zero baseCurrency would cause payout limit lookups
|
|
398
|
+
// in fulfillCommitmentsOf to silently fail, skipping all commitment payouts.
|
|
399
|
+
// slither-disable-next-line incorrect-equality
|
|
400
|
+
if (launchProjectData.token.token != JBConstants.NATIVE_TOKEN && launchProjectData.token.currency == 0) {
|
|
401
|
+
revert DefifaDeployer_InvalidCurrency();
|
|
402
|
+
}
|
|
403
|
+
|
|
400
404
|
// Get the game ID, optimistically knowing it will be one greater than the current count.
|
|
401
405
|
// Note: this prediction can race with other concurrent project deployments. If another project is
|
|
402
406
|
// created between reading count() and launchProjectFor(), the actual ID will differ. This is
|
package/src/DefifaHook.sol
CHANGED
|
@@ -131,6 +131,10 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
131
131
|
/// @notice The contract reporting the game pot.
|
|
132
132
|
IDefifaGamePotReporter public override gamePotReporter;
|
|
133
133
|
|
|
134
|
+
/// @notice Whether a token was minted through reserves (free) rather than paid for.
|
|
135
|
+
/// @dev Reserve-minted tokens are excluded from refund calculations since no funds were contributed for them.
|
|
136
|
+
mapping(uint256 tokenId => bool) public override isReserveMint;
|
|
137
|
+
|
|
134
138
|
/// @notice The currency that is accepted when minting tier NFTs.
|
|
135
139
|
uint256 public override pricingCurrency;
|
|
136
140
|
|
|
@@ -289,6 +293,27 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
289
293
|
tokenIds: decodedTokenIds, hookStore: hookStore, hook: address(this)
|
|
290
294
|
});
|
|
291
295
|
|
|
296
|
+
// During refund phases, exclude reserve-minted tokens — they were minted for free and have no paid amount
|
|
297
|
+
// to refund.
|
|
298
|
+
if (
|
|
299
|
+
gamePhase == DefifaGamePhase.MINT || gamePhase == DefifaGamePhase.REFUND
|
|
300
|
+
|| gamePhase == DefifaGamePhase.NO_CONTEST
|
|
301
|
+
) {
|
|
302
|
+
for (uint256 i; i < decodedTokenIds.length;) {
|
|
303
|
+
if (isReserveMint[decodedTokenIds[i]]) {
|
|
304
|
+
// slither-disable-next-line calls-loop
|
|
305
|
+
cumulativeMintPrice -= hookStore.tierOfTokenId({
|
|
306
|
+
hook: address(this), tokenId: decodedTokenIds[i], includeResolvedUri: false
|
|
307
|
+
})
|
|
308
|
+
.price;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
unchecked {
|
|
312
|
+
++i;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
292
317
|
// Use this contract as the only cash out hook.
|
|
293
318
|
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
294
319
|
hookSpecifications[0] =
|
|
@@ -599,6 +624,9 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
599
624
|
// Set the token ID.
|
|
600
625
|
tokenId = tokenIds[i];
|
|
601
626
|
|
|
627
|
+
// Flag this token as reserve-minted so it is excluded from refund calculations.
|
|
628
|
+
isReserveMint[tokenId] = true;
|
|
629
|
+
|
|
602
630
|
// Mint the token to the reserve beneficiary.
|
|
603
631
|
// slither-disable-next-line reentrancy-no-eth
|
|
604
632
|
_mint({to: reservedTokenBeneficiary, tokenId: tokenId});
|
|
@@ -162,6 +162,11 @@ interface IDefifaHook is IJB721Hook {
|
|
|
162
162
|
/// @return The total attestation units.
|
|
163
163
|
function getTierTotalAttestationUnitsOf(uint256 tier) external view returns (uint256);
|
|
164
164
|
|
|
165
|
+
/// @notice Whether a token was minted through reserves rather than paid for.
|
|
166
|
+
/// @param tokenId The ID of the token to check.
|
|
167
|
+
/// @return True if the token was minted as a reserve.
|
|
168
|
+
function isReserveMint(uint256 tokenId) external view returns (bool);
|
|
169
|
+
|
|
165
170
|
/// @notice The pricing currency used by this hook.
|
|
166
171
|
/// @return The currency identifier.
|
|
167
172
|
function pricingCurrency() external view returns (uint256);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {DefifaUSDCTest} from "../DefifaUSDC.t.sol";
|
|
5
|
+
import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
|
|
6
|
+
import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
|
|
7
|
+
import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
|
|
8
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
9
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
10
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
11
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
12
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
13
|
+
|
|
14
|
+
/// @notice Regression test for the currency mismatch fix: ERC-20 games now correctly resolve payout limits via
|
|
15
|
+
/// baseCurrency. Before the fix, using a non-canonical currency (e.g. currency=1 for USDC) caused sendPayoutsOf to use
|
|
16
|
+
/// uint32(uint160(token)) which didn't match the stored payout limit currency, silently skipping payouts.
|
|
17
|
+
/// After the fix, fulfillCommitmentsOf uses metadata.baseCurrency which always matches the stored limit.
|
|
18
|
+
contract CodexNemesisCurrencyMismatchBypassTest is DefifaUSDCTest {
|
|
19
|
+
/// @notice Verify that an ERC-20 game with non-canonical currency (1) correctly pays out commitment fees.
|
|
20
|
+
function test_nonCanonicalCurrencyPayoutsNowSucceed() external {
|
|
21
|
+
uint104 tierPrice = 100e6;
|
|
22
|
+
|
|
23
|
+
(_pid, _nft, _gov) = _launch(_launchDataUsdcNonCanonical(tierPrice));
|
|
24
|
+
_users = new address[](2);
|
|
25
|
+
_users[0] = _addr(0);
|
|
26
|
+
_users[1] = _addr(1);
|
|
27
|
+
|
|
28
|
+
vm.warp(block.timestamp + 1 days + 1);
|
|
29
|
+
|
|
30
|
+
_mintUsdc(_users[0], 1, tierPrice);
|
|
31
|
+
_mintUsdc(_users[1], 2, tierPrice);
|
|
32
|
+
_delegateSelf(_users[0], 1);
|
|
33
|
+
_delegateSelf(_users[1], 2);
|
|
34
|
+
|
|
35
|
+
vm.warp(block.timestamp + 2 days);
|
|
36
|
+
|
|
37
|
+
DefifaTierCashOutWeight[] memory scorecard = new DefifaTierCashOutWeight[](2);
|
|
38
|
+
scorecard[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
|
|
39
|
+
scorecard[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
|
|
40
|
+
|
|
41
|
+
vm.prank(_users[1]);
|
|
42
|
+
uint256 scorecardId = _gov.submitScorecardFor(_pid, scorecard);
|
|
43
|
+
|
|
44
|
+
vm.prank(_users[1]);
|
|
45
|
+
_gov.attestToScorecardFrom(_pid, scorecardId);
|
|
46
|
+
|
|
47
|
+
vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_pid) + 1);
|
|
48
|
+
|
|
49
|
+
uint256 preRatificationBalance = _balance();
|
|
50
|
+
uint256 expectedFee = (preRatificationBalance * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
51
|
+
|
|
52
|
+
vm.prank(_users[1]);
|
|
53
|
+
_gov.ratifyScorecardFrom(_pid, scorecard);
|
|
54
|
+
|
|
55
|
+
// After the fix: payout succeeds, fulfilledCommitmentsOf stores the actual fee (not the sentinel).
|
|
56
|
+
assertEq(
|
|
57
|
+
deployer.fulfilledCommitmentsOf(_pid), expectedFee, "fulfilled commitments equals the expected fee amount"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// The fee has been paid out, reducing the game pot.
|
|
61
|
+
assertEq(_balance(), preRatificationBalance - expectedFee, "balance decreased by the fee amount");
|
|
62
|
+
|
|
63
|
+
// Winner cashes out and receives only the post-fee surplus, not the full pot.
|
|
64
|
+
uint256 winnerBalBefore = usdc.balanceOf(_users[0]);
|
|
65
|
+
_cashOutUsdc(_users[0], 1, 1);
|
|
66
|
+
uint256 winnerReceived = usdc.balanceOf(_users[0]) - winnerBalBefore;
|
|
67
|
+
|
|
68
|
+
assertEq(
|
|
69
|
+
winnerReceived,
|
|
70
|
+
preRatificationBalance - expectedFee,
|
|
71
|
+
"winner receives the post-fee surplus, not the full pot"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _launchDataUsdcNonCanonical(uint104 tierPrice) internal returns (DefifaLaunchProjectData memory) {
|
|
76
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](2);
|
|
77
|
+
for (uint256 i; i < 2; i++) {
|
|
78
|
+
tp[i] = DefifaTierParams({
|
|
79
|
+
reservedRate: 1001,
|
|
80
|
+
reservedTokenBeneficiary: address(0),
|
|
81
|
+
encodedIPFSUri: bytes32(0),
|
|
82
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
83
|
+
name: "DEFIFA"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Non-canonical currency (1 = ETH currency ID) for a USDC token.
|
|
88
|
+
// Before the fix, this caused fulfillCommitmentsOf to silently skip payouts.
|
|
89
|
+
return DefifaLaunchProjectData({
|
|
90
|
+
name: "DEFIFA_USDC_NONCANONICAL",
|
|
91
|
+
projectUri: "",
|
|
92
|
+
contractUri: "",
|
|
93
|
+
baseUri: "",
|
|
94
|
+
token: JBAccountingContext({token: address(usdc), decimals: 6, currency: 1}),
|
|
95
|
+
mintPeriodDuration: 1 days,
|
|
96
|
+
start: uint48(block.timestamp + 3 days),
|
|
97
|
+
refundPeriodDuration: 1 days,
|
|
98
|
+
store: new JB721TiersHookStore(),
|
|
99
|
+
splits: new JBSplit[](0),
|
|
100
|
+
attestationStartTime: 0,
|
|
101
|
+
attestationGracePeriod: 1 days,
|
|
102
|
+
defaultAttestationDelegate: address(0),
|
|
103
|
+
tierPrice: tierPrice,
|
|
104
|
+
tiers: tp,
|
|
105
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
106
|
+
terminal: jbMultiTerminal(),
|
|
107
|
+
minParticipation: 0,
|
|
108
|
+
scorecardTimeout: 0,
|
|
109
|
+
timelockDuration: 0
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
5
|
+
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
6
|
+
|
|
7
|
+
import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
|
|
8
|
+
import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
|
|
9
|
+
import {DefifaHook} from "../../src/DefifaHook.sol";
|
|
10
|
+
import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
|
|
11
|
+
import {DefifaGamePhase} from "../../src/enums/DefifaGamePhase.sol";
|
|
12
|
+
import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
|
|
13
|
+
import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
|
|
14
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
15
|
+
import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
|
|
16
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
17
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
18
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
19
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
20
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
21
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
22
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
23
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
24
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
25
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
26
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
27
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
28
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
29
|
+
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
30
|
+
|
|
31
|
+
contract CodexNemesisNoContestReserveDrainTest is JBTest, TestBaseWorkflow {
|
|
32
|
+
uint256 internal _protocolFeeProjectId;
|
|
33
|
+
uint256 internal _defifaProjectId;
|
|
34
|
+
|
|
35
|
+
DefifaDeployer internal _deployer;
|
|
36
|
+
DefifaHook internal _hookImpl;
|
|
37
|
+
DefifaGovernor internal _governorImpl;
|
|
38
|
+
|
|
39
|
+
address internal _projectOwner = address(bytes20(keccak256("projectOwner")));
|
|
40
|
+
address internal _player = address(bytes20(keccak256("player")));
|
|
41
|
+
address internal _reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
|
|
42
|
+
|
|
43
|
+
function setUp() public virtual override {
|
|
44
|
+
super.setUp();
|
|
45
|
+
|
|
46
|
+
JBAccountingContext[] memory tokens = new JBAccountingContext[](1);
|
|
47
|
+
tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
|
|
48
|
+
|
|
49
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
50
|
+
terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
|
|
51
|
+
|
|
52
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
53
|
+
rulesetConfigs[0] = JBRulesetConfig({
|
|
54
|
+
mustStartAtOrAfter: 0,
|
|
55
|
+
duration: 10 days,
|
|
56
|
+
weight: 1e18,
|
|
57
|
+
weightCutPercent: 0,
|
|
58
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
59
|
+
metadata: JBRulesetMetadata({
|
|
60
|
+
reservedPercent: 0,
|
|
61
|
+
cashOutTaxRate: 0,
|
|
62
|
+
baseCurrency: JBCurrencyIds.ETH,
|
|
63
|
+
pausePay: false,
|
|
64
|
+
pauseCreditTransfers: false,
|
|
65
|
+
allowOwnerMinting: false,
|
|
66
|
+
allowSetCustomToken: false,
|
|
67
|
+
allowTerminalMigration: false,
|
|
68
|
+
allowSetTerminals: false,
|
|
69
|
+
allowSetController: false,
|
|
70
|
+
allowAddAccountingContext: false,
|
|
71
|
+
allowAddPriceFeed: false,
|
|
72
|
+
ownerMustSendPayouts: false,
|
|
73
|
+
holdFees: false,
|
|
74
|
+
useTotalSurplusForCashOuts: false,
|
|
75
|
+
useDataHookForPay: true,
|
|
76
|
+
useDataHookForCashOut: true,
|
|
77
|
+
dataHook: address(0),
|
|
78
|
+
metadata: 0
|
|
79
|
+
}),
|
|
80
|
+
splitGroups: new JBSplitGroup[](0),
|
|
81
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
_protocolFeeProjectId =
|
|
85
|
+
jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
86
|
+
vm.prank(_projectOwner);
|
|
87
|
+
address nanaToken =
|
|
88
|
+
address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
|
|
89
|
+
|
|
90
|
+
_defifaProjectId =
|
|
91
|
+
jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
|
|
92
|
+
vm.prank(_projectOwner);
|
|
93
|
+
address defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
|
|
94
|
+
|
|
95
|
+
_hookImpl = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
|
|
96
|
+
_governorImpl = new DefifaGovernor(jbController(), address(this));
|
|
97
|
+
_deployer = new DefifaDeployer(
|
|
98
|
+
address(_hookImpl),
|
|
99
|
+
new DefifaTokenUriResolver(ITypeface(address(0))),
|
|
100
|
+
_governorImpl,
|
|
101
|
+
jbController(),
|
|
102
|
+
new JBAddressRegistry(),
|
|
103
|
+
_defifaProjectId,
|
|
104
|
+
_protocolFeeProjectId
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
_hookImpl.transferOwnership(address(_deployer));
|
|
108
|
+
_governorImpl.transferOwnership(address(_deployer));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function test_noContestReserveMintExcludedFromRefund() external {
|
|
112
|
+
DefifaLaunchProjectData memory data = _launchData();
|
|
113
|
+
uint256 projectId = _deployer.launchGameWith(data);
|
|
114
|
+
|
|
115
|
+
vm.warp(data.start - data.mintPeriodDuration - data.refundPeriodDuration);
|
|
116
|
+
vm.deal(_player, 1 ether);
|
|
117
|
+
|
|
118
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
119
|
+
tierIds[0] = 1;
|
|
120
|
+
vm.prank(_player);
|
|
121
|
+
jbMultiTerminal().pay{value: 1 ether}(
|
|
122
|
+
projectId,
|
|
123
|
+
JBConstants.NATIVE_TOKEN,
|
|
124
|
+
1 ether,
|
|
125
|
+
_player,
|
|
126
|
+
0,
|
|
127
|
+
"",
|
|
128
|
+
_buildPayMetadata(abi.encode(_player, tierIds))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
vm.warp(data.start + 1);
|
|
132
|
+
(, JBRulesetMetadata memory metadata) = jbController().currentRulesetOf(projectId);
|
|
133
|
+
DefifaHook hook = DefifaHook(metadata.dataHook);
|
|
134
|
+
assertEq(uint256(_deployer.currentGamePhaseOf(projectId)), uint256(DefifaGamePhase.NO_CONTEST));
|
|
135
|
+
|
|
136
|
+
JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
|
|
137
|
+
reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
|
|
138
|
+
hook.mintReservesFor(reserveConfigs);
|
|
139
|
+
|
|
140
|
+
assertEq(hook.balanceOf(_reserveBeneficiary), 1, "reserve beneficiary received a free NFT");
|
|
141
|
+
assertTrue(hook.isReserveMint(_generateTokenId(1, 2)), "token flagged as reserve mint");
|
|
142
|
+
|
|
143
|
+
_deployer.triggerNoContestFor(projectId);
|
|
144
|
+
|
|
145
|
+
// Build metadata for the reserve token cashout before calling expectRevert.
|
|
146
|
+
uint256 reserveTokenId = _generateTokenId(1, 2);
|
|
147
|
+
uint256[] memory reserveTokenIds = new uint256[](1);
|
|
148
|
+
reserveTokenIds[0] = reserveTokenId;
|
|
149
|
+
bytes memory reserveCashOutMetadata = _buildCashOutMetadata(reserveTokenIds);
|
|
150
|
+
|
|
151
|
+
// The reserve beneficiary's cashout reverts — reserve-minted tokens are excluded from refund calculations.
|
|
152
|
+
vm.prank(_reserveBeneficiary);
|
|
153
|
+
vm.expectRevert();
|
|
154
|
+
jbMultiTerminal()
|
|
155
|
+
.cashOutTokensOf(
|
|
156
|
+
_reserveBeneficiary,
|
|
157
|
+
projectId,
|
|
158
|
+
0,
|
|
159
|
+
JBConstants.NATIVE_TOKEN,
|
|
160
|
+
0,
|
|
161
|
+
payable(_reserveBeneficiary),
|
|
162
|
+
reserveCashOutMetadata
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// The paid player can still get their full refund.
|
|
166
|
+
uint256 playerTokenId = _generateTokenId(1, 1);
|
|
167
|
+
uint256 balanceBefore = _player.balance;
|
|
168
|
+
_cashOut(projectId, _player, playerTokenId);
|
|
169
|
+
|
|
170
|
+
// Player gets full refund (1 ether minus fee).
|
|
171
|
+
assertTrue(_player.balance > balanceBefore, "player received refund");
|
|
172
|
+
assertEq(hook.balanceOf(_player), 0, "player NFT burned");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _cashOut(uint256 projectId, address holder, uint256 tokenId) internal {
|
|
176
|
+
uint256[] memory tokenIds = new uint256[](1);
|
|
177
|
+
tokenIds[0] = tokenId;
|
|
178
|
+
bytes memory cashOutMetadata = _buildCashOutMetadata(tokenIds);
|
|
179
|
+
|
|
180
|
+
vm.prank(holder);
|
|
181
|
+
jbMultiTerminal()
|
|
182
|
+
.cashOutTokensOf(holder, projectId, 0, JBConstants.NATIVE_TOKEN, 0, payable(holder), cashOutMetadata);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _launchData() internal returns (DefifaLaunchProjectData memory) {
|
|
186
|
+
DefifaTierParams[] memory tierParams = new DefifaTierParams[](1);
|
|
187
|
+
tierParams[0] = DefifaTierParams({
|
|
188
|
+
reservedRate: 1,
|
|
189
|
+
reservedTokenBeneficiary: _reserveBeneficiary,
|
|
190
|
+
encodedIPFSUri: bytes32(0),
|
|
191
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
192
|
+
name: "SOLE"
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return DefifaLaunchProjectData({
|
|
196
|
+
name: "DEFIFA",
|
|
197
|
+
projectUri: "",
|
|
198
|
+
contractUri: "",
|
|
199
|
+
baseUri: "",
|
|
200
|
+
tiers: tierParams,
|
|
201
|
+
tierPrice: 1 ether,
|
|
202
|
+
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
203
|
+
mintPeriodDuration: 1 days,
|
|
204
|
+
refundPeriodDuration: 1 days,
|
|
205
|
+
start: uint48(block.timestamp + 3 days),
|
|
206
|
+
splits: new JBSplit[](0),
|
|
207
|
+
attestationStartTime: 0,
|
|
208
|
+
attestationGracePeriod: 1 days,
|
|
209
|
+
defaultAttestationDelegate: address(0),
|
|
210
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
211
|
+
terminal: jbMultiTerminal(),
|
|
212
|
+
store: new JB721TiersHookStore(),
|
|
213
|
+
minParticipation: 2 ether,
|
|
214
|
+
scorecardTimeout: 0,
|
|
215
|
+
timelockDuration: 0
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _buildPayMetadata(bytes memory decodedData) internal view returns (bytes memory) {
|
|
220
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
221
|
+
ids[0] = metadataHelper().getId("pay", address(_hookImpl));
|
|
222
|
+
bytes[] memory datas = new bytes[](1);
|
|
223
|
+
datas[0] = decodedData;
|
|
224
|
+
return metadataHelper().createMetadata(ids, datas);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _buildCashOutMetadata(uint256[] memory tokenIds) internal view returns (bytes memory) {
|
|
228
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
229
|
+
ids[0] = metadataHelper().getId("cashOut", address(_hookImpl));
|
|
230
|
+
bytes[] memory datas = new bytes[](1);
|
|
231
|
+
datas[0] = abi.encode(tokenIds);
|
|
232
|
+
return metadataHelper().createMetadata(ids, datas);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) {
|
|
236
|
+
return (tierId * 1_000_000_000) + tokenNumber;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// SPDX-License-Identifier: UNLICENSED
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {DefifaUSDCTest, DefifaMockUSDC} from "../DefifaUSDC.t.sol";
|
|
5
|
+
import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
|
|
6
|
+
import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
|
|
7
|
+
import {DefifaHook} from "../../src/DefifaHook.sol";
|
|
8
|
+
import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
|
|
9
|
+
import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
|
|
10
|
+
import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
|
|
11
|
+
import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
|
|
12
|
+
import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
|
|
13
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
14
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
15
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
16
|
+
import {JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
17
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
18
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
19
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
20
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
21
|
+
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
22
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
23
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
24
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
25
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
26
|
+
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
27
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
28
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
29
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
30
|
+
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
31
|
+
import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
|
|
32
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
33
|
+
import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
|
|
34
|
+
|
|
35
|
+
/// @title CurrencyMismatchFixTest
|
|
36
|
+
/// @notice Adversarial tests for the currency mismatch fix: verifies that fulfillCommitmentsOf correctly resolves
|
|
37
|
+
/// payout limits for both ETH and ERC-20 games, and that launch-time validation rejects zero-currency ERC-20
|
|
38
|
+
/// configurations. Inherits DefifaUSDCTest for USDC helpers and fee project setup.
|
|
39
|
+
contract CurrencyMismatchFixTest is DefifaUSDCTest {
|
|
40
|
+
// =========================================================================
|
|
41
|
+
// HELPERS
|
|
42
|
+
// =========================================================================
|
|
43
|
+
|
|
44
|
+
function _launchDataNonCanonical(uint8 n, uint104 tierPrice) internal returns (DefifaLaunchProjectData memory) {
|
|
45
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](n);
|
|
46
|
+
for (uint256 i; i < n; i++) {
|
|
47
|
+
tp[i] = DefifaTierParams({
|
|
48
|
+
reservedRate: 1001,
|
|
49
|
+
reservedTokenBeneficiary: address(0),
|
|
50
|
+
encodedIPFSUri: bytes32(0),
|
|
51
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
52
|
+
name: "DEFIFA"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Non-canonical currency (1) for a USDC token.
|
|
57
|
+
return DefifaLaunchProjectData({
|
|
58
|
+
name: "DEFIFA_NONCANONICAL",
|
|
59
|
+
projectUri: "",
|
|
60
|
+
contractUri: "",
|
|
61
|
+
baseUri: "",
|
|
62
|
+
token: JBAccountingContext({token: address(usdc), decimals: 6, currency: 1}),
|
|
63
|
+
mintPeriodDuration: 1 days,
|
|
64
|
+
start: uint48(block.timestamp + 3 days),
|
|
65
|
+
refundPeriodDuration: 1 days,
|
|
66
|
+
store: new JB721TiersHookStore(),
|
|
67
|
+
splits: new JBSplit[](0),
|
|
68
|
+
attestationStartTime: 0,
|
|
69
|
+
attestationGracePeriod: 1 days,
|
|
70
|
+
defaultAttestationDelegate: address(0),
|
|
71
|
+
tierPrice: tierPrice,
|
|
72
|
+
tiers: tp,
|
|
73
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
74
|
+
terminal: jbMultiTerminal(),
|
|
75
|
+
minParticipation: 0,
|
|
76
|
+
scorecardTimeout: 0,
|
|
77
|
+
timelockDuration: 0
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _setupNonCanonicalGame(uint8 nTiers, uint104 tierPrice) internal {
|
|
82
|
+
DefifaLaunchProjectData memory d = _launchDataNonCanonical(nTiers, tierPrice);
|
|
83
|
+
(_pid, _nft, _gov) = _launch(d);
|
|
84
|
+
vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
|
|
85
|
+
_users = new address[](nTiers);
|
|
86
|
+
for (uint256 i; i < nTiers; i++) {
|
|
87
|
+
_users[i] = _addr(i);
|
|
88
|
+
_mintUsdc(_users[i], i + 1, tierPrice);
|
|
89
|
+
_delegateSelf(_users[i], i + 1);
|
|
90
|
+
vm.warp(block.timestamp + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =========================================================================
|
|
95
|
+
// TEST 1: ERC-20 game with non-canonical currency correctly resolves payout limit
|
|
96
|
+
// =========================================================================
|
|
97
|
+
|
|
98
|
+
/// @notice An ERC-20 game launched with currency=1 (non-canonical) now correctly sends commitment payouts
|
|
99
|
+
/// because fulfillCommitmentsOf uses metadata.baseCurrency instead of uint32(uint160(token)).
|
|
100
|
+
function test_currencyMismatchFix_erc20NonCanonicalCurrencyFulfillsCorrectly() external {
|
|
101
|
+
uint104 tierPrice = 100e6;
|
|
102
|
+
_setupNonCanonicalGame(4, tierPrice);
|
|
103
|
+
|
|
104
|
+
// Advance to scoring phase.
|
|
105
|
+
_toScoring();
|
|
106
|
+
|
|
107
|
+
uint256 potBefore = _balance();
|
|
108
|
+
assertEq(potBefore, 400e6, "pot = 400 USDC before fulfillment");
|
|
109
|
+
|
|
110
|
+
uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
111
|
+
|
|
112
|
+
_attestAndRatify(_evenScorecard(4));
|
|
113
|
+
|
|
114
|
+
// Verify: payout succeeded. fulfilledCommitmentsOf stores the actual fee amount, not sentinel (1).
|
|
115
|
+
uint256 fulfilled = deployer.fulfilledCommitmentsOf(_pid);
|
|
116
|
+
assertEq(fulfilled, expectedFee, "fulfilled = expected fee (payout succeeded)");
|
|
117
|
+
|
|
118
|
+
// Verify: balance reduced by the fee.
|
|
119
|
+
assertEq(_balance(), potBefore - expectedFee, "balance = pot - fee");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// =========================================================================
|
|
123
|
+
// TEST 2: ETH game fulfillment still works correctly (no regression)
|
|
124
|
+
// =========================================================================
|
|
125
|
+
|
|
126
|
+
/// @notice ETH game (canonical currency) continues to work after the fix.
|
|
127
|
+
/// Covered by DefifaFeeAccountingTest; this verifies no regression from the baseCurrency change.
|
|
128
|
+
function test_currencyMismatchFix_ethGameFeeAccountingUnchanged() external {
|
|
129
|
+
// The DefifaFeeAccountingTest suite tests ETH fulfillment comprehensively.
|
|
130
|
+
// Here we verify the core assertion: fee percentage matches for USDC canonical game too.
|
|
131
|
+
uint104 tierPrice = 100e6;
|
|
132
|
+
_setupGameUsdc(4, tierPrice);
|
|
133
|
+
|
|
134
|
+
_toScoring();
|
|
135
|
+
|
|
136
|
+
uint256 potBefore = _balance();
|
|
137
|
+
uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
138
|
+
|
|
139
|
+
_attestAndRatify(_evenScorecard(4));
|
|
140
|
+
|
|
141
|
+
uint256 fulfilled = deployer.fulfilledCommitmentsOf(_pid);
|
|
142
|
+
assertEq(fulfilled, expectedFee, "canonical USDC fee unchanged by fix");
|
|
143
|
+
assertEq(fulfilled + _balance(), potBefore, "fee + surplus = original pot");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =========================================================================
|
|
147
|
+
// TEST 3: Non-canonical ERC-20 winner cash-out is correct after fulfillment
|
|
148
|
+
// =========================================================================
|
|
149
|
+
|
|
150
|
+
/// @notice After the fix, the winner of a non-canonical-currency ERC-20 game receives the post-fee surplus
|
|
151
|
+
/// (not the full pot). This confirms the fee was actually deducted.
|
|
152
|
+
function test_currencyMismatchFix_winnerReceivesPostFeeSurplusNotFullPot() external {
|
|
153
|
+
uint104 tierPrice = 100e6;
|
|
154
|
+
_setupNonCanonicalGame(2, tierPrice);
|
|
155
|
+
|
|
156
|
+
_toScoring();
|
|
157
|
+
|
|
158
|
+
uint256 potBefore = _balance();
|
|
159
|
+
uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
|
|
160
|
+
|
|
161
|
+
// Tier 1 gets 100% of the cashout weight.
|
|
162
|
+
DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](2);
|
|
163
|
+
sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
|
|
164
|
+
sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
|
|
165
|
+
_attestAndRatify(sc);
|
|
166
|
+
|
|
167
|
+
// Fulfillment succeeded -- fee was deducted.
|
|
168
|
+
assertEq(deployer.fulfilledCommitmentsOf(_pid), expectedFee, "fee was deducted");
|
|
169
|
+
|
|
170
|
+
// Winner cashes out and receives the post-fee surplus.
|
|
171
|
+
uint256 winnerBalBefore = usdc.balanceOf(_users[0]);
|
|
172
|
+
_cashOutUsdc(_users[0], 1, 1);
|
|
173
|
+
uint256 winnerReceived = usdc.balanceOf(_users[0]) - winnerBalBefore;
|
|
174
|
+
|
|
175
|
+
// Before the fix, winner would have received the full pot (fee was skipped).
|
|
176
|
+
// After the fix, winner receives pot minus fee.
|
|
177
|
+
assertEq(winnerReceived, potBefore - expectedFee, "winner receives post-fee surplus, not the full pot");
|
|
178
|
+
assertLt(winnerReceived, potBefore, "winner does NOT receive the full pot");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =========================================================================
|
|
182
|
+
// TEST 4: Launch rejects zero currency for non-native tokens
|
|
183
|
+
// =========================================================================
|
|
184
|
+
|
|
185
|
+
/// @notice Launching an ERC-20 game with currency=0 is rejected at launch time.
|
|
186
|
+
function test_currencyMismatchFix_revertOnZeroCurrencyForErc20() external {
|
|
187
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](2);
|
|
188
|
+
for (uint256 i; i < 2; i++) {
|
|
189
|
+
tp[i] = DefifaTierParams({
|
|
190
|
+
reservedRate: 1001,
|
|
191
|
+
reservedTokenBeneficiary: address(0),
|
|
192
|
+
encodedIPFSUri: bytes32(0),
|
|
193
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
194
|
+
name: "DEFIFA"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
DefifaLaunchProjectData memory d = DefifaLaunchProjectData({
|
|
199
|
+
name: "DEFIFA_ZERO_CURRENCY",
|
|
200
|
+
projectUri: "",
|
|
201
|
+
contractUri: "",
|
|
202
|
+
baseUri: "",
|
|
203
|
+
token: JBAccountingContext({token: address(usdc), decimals: 6, currency: 0}),
|
|
204
|
+
mintPeriodDuration: 1 days,
|
|
205
|
+
start: uint48(block.timestamp + 3 days),
|
|
206
|
+
refundPeriodDuration: 1 days,
|
|
207
|
+
store: new JB721TiersHookStore(),
|
|
208
|
+
splits: new JBSplit[](0),
|
|
209
|
+
attestationStartTime: 0,
|
|
210
|
+
attestationGracePeriod: 1 days,
|
|
211
|
+
defaultAttestationDelegate: address(0),
|
|
212
|
+
tierPrice: 100e6,
|
|
213
|
+
tiers: tp,
|
|
214
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
215
|
+
terminal: jbMultiTerminal(),
|
|
216
|
+
minParticipation: 0,
|
|
217
|
+
scorecardTimeout: 0,
|
|
218
|
+
timelockDuration: 0
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
vm.expectRevert(abi.encodeWithSignature("DefifaDeployer_InvalidCurrency()"));
|
|
222
|
+
deployer.launchGameWith(d);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// @notice Native token (ETH) is exempt from the zero-currency check.
|
|
226
|
+
function test_currencyMismatchFix_nativeTokenAllowsAnyCurrency() external {
|
|
227
|
+
DefifaTierParams[] memory tp = new DefifaTierParams[](2);
|
|
228
|
+
for (uint256 i; i < 2; i++) {
|
|
229
|
+
tp[i] = DefifaTierParams({
|
|
230
|
+
reservedRate: 1001,
|
|
231
|
+
reservedTokenBeneficiary: address(0),
|
|
232
|
+
encodedIPFSUri: bytes32(0),
|
|
233
|
+
shouldUseReservedTokenBeneficiaryAsDefault: false,
|
|
234
|
+
name: "DEFIFA"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
DefifaLaunchProjectData memory d = DefifaLaunchProjectData({
|
|
239
|
+
name: "DEFIFA_ETH_LAUNCH",
|
|
240
|
+
projectUri: "",
|
|
241
|
+
contractUri: "",
|
|
242
|
+
baseUri: "",
|
|
243
|
+
token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
|
|
244
|
+
mintPeriodDuration: 1 days,
|
|
245
|
+
start: uint48(block.timestamp + 3 days),
|
|
246
|
+
refundPeriodDuration: 1 days,
|
|
247
|
+
store: new JB721TiersHookStore(),
|
|
248
|
+
splits: new JBSplit[](0),
|
|
249
|
+
attestationStartTime: 0,
|
|
250
|
+
attestationGracePeriod: 1 days,
|
|
251
|
+
defaultAttestationDelegate: address(0),
|
|
252
|
+
tierPrice: 1 ether,
|
|
253
|
+
tiers: tp,
|
|
254
|
+
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
255
|
+
terminal: jbMultiTerminal(),
|
|
256
|
+
minParticipation: 0,
|
|
257
|
+
scorecardTimeout: 0,
|
|
258
|
+
timelockDuration: 0
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Should succeed without revert.
|
|
262
|
+
uint256 gameId = deployer.launchGameWith(d);
|
|
263
|
+
assertGt(gameId, 0, "ETH game launched successfully");
|
|
264
|
+
}
|
|
265
|
+
}
|