@ballkidz/defifa 0.0.22 → 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/ADMINISTRATION.md +55 -142
- package/ARCHITECTURE.md +109 -45
- package/AUDIT_INSTRUCTIONS.md +50 -52
- package/CHANGELOG.md +4 -0
- package/CRYPTO_ECON.md +2 -2
- package/README.md +97 -32
- package/RISKS.md +41 -50
- package/SKILLS.md +18 -14
- package/USER_JOURNEYS.md +142 -42
- package/foundry.toml +2 -0
- package/package.json +1 -1
- package/references/operations.md +6 -1
- package/references/runtime.md +15 -4
- package/src/DefifaDeployer.sol +14 -7
- package/src/DefifaHook.sol +28 -0
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/src/structs/DefifaTierParams.sol +0 -1
- package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +112 -0
- package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +238 -0
- package/test/audit/CodexTierCapMismatch.t.sol +171 -0
- package/test/audit/CurrencyMismatchFix.t.sol +265 -0
- package/test/audit/H5TierCapValidation.t.sol +184 -0
package/package.json
CHANGED
package/references/operations.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Defifa Operations
|
|
2
2
|
|
|
3
|
+
Use this file when the task is about launch config, phase timing, governance windows, or deciding whether a symptom is operational drift or runtime logic.
|
|
4
|
+
|
|
3
5
|
## Deployment Surface
|
|
4
6
|
|
|
5
7
|
- [`src/DefifaDeployer.sol`](../src/DefifaDeployer.sol) is the first stop for launch-time config, phase queueing, and post-ratification fulfillment.
|
|
6
|
-
- [`script/Deploy.s.sol`](../script/Deploy.s.sol)
|
|
8
|
+
- [`script/Deploy.s.sol`](../script/Deploy.s.sol) and [`script/helpers/DefifaDeploymentLib.sol`](../script/helpers/DefifaDeploymentLib.sol) are the deployment entrypoints when the task is about current wiring rather than game mechanics.
|
|
7
9
|
- [`src/structs/`](../src/structs/) and [`src/enums/`](../src/enums/) define launch data, phase types, and other inputs that often drift from remembered assumptions.
|
|
8
10
|
|
|
9
11
|
## Change Checklist
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
- If you edit hook settlement logic, re-check fee accounting and mint-cost invariants.
|
|
13
15
|
- If you touch governance thresholds or attestation behavior, inspect the governor tests before assuming the change is local.
|
|
14
16
|
- If you touch token metadata or rendering, verify whether the bug belongs in the resolver instead of settlement code.
|
|
17
|
+
- If you touch anything supply-sensitive, inspect the audit tests around pending reserves and quorum before relying on current intuition.
|
|
15
18
|
|
|
16
19
|
## Common Failure Modes
|
|
17
20
|
|
|
@@ -19,9 +22,11 @@
|
|
|
19
22
|
- Governance behavior looks wrong, but the real issue is stale launch configuration.
|
|
20
23
|
- Settlement changes accidentally affect fee distribution or redemption accounting.
|
|
21
24
|
- Resolver issues get misdiagnosed as hook or governor problems because they surface through NFTs.
|
|
25
|
+
- Audit-style failures around reserve dilution or attestation counting are treated as isolated math issues even though they cross deployer, hook, and governor boundaries.
|
|
22
26
|
|
|
23
27
|
## Useful Proof Points
|
|
24
28
|
|
|
25
29
|
- [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
|
|
26
30
|
- [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) and [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) for pinned edge cases.
|
|
27
31
|
- [`test/BWAFunctionComparison.t.sol`](../test/BWAFunctionComparison.t.sol) and [`test/DefifaUSDC.t.sol`](../test/DefifaUSDC.t.sol) when currency or accounting context matters.
|
|
32
|
+
- [`test/audit/`](../test/audit/) when a change touches pending reserves, registry alignment, quorum griefing, or double-counting.
|
package/references/runtime.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Defifa Runtime
|
|
2
2
|
|
|
3
|
+
Use this file when `defifa/SKILLS.md` has already routed you here and you need to reason about the game as a state machine rather than as isolated contracts.
|
|
4
|
+
|
|
3
5
|
## Contract Roles
|
|
4
6
|
|
|
5
7
|
- [`src/DefifaDeployer.sol`](../src/DefifaDeployer.sol) launches games, manages phase progression, fulfills commitments, and triggers safety exits such as no-contest.
|
|
@@ -12,9 +14,9 @@
|
|
|
12
14
|
|
|
13
15
|
1. Countdown before minting opens.
|
|
14
16
|
2. Mint phase where players buy outcome NFTs and can delegate attestation power.
|
|
15
|
-
3. Optional refund phase.
|
|
17
|
+
3. Optional refund phase if the launch configuration allows it.
|
|
16
18
|
4. Scoring phase where scorecards are submitted, attested, and ratified.
|
|
17
|
-
5. Complete or no-contest settlement depending on governance and safety
|
|
19
|
+
5. Complete or no-contest settlement depending on governance outcome and safety checks.
|
|
18
20
|
|
|
19
21
|
## High-Risk Areas
|
|
20
22
|
|
|
@@ -22,11 +24,20 @@
|
|
|
22
24
|
- No-contest and refund behavior: these paths are economic safety valves, not edge-case garnish.
|
|
23
25
|
- Fee accounting and commitment fulfillment: payout ordering and accounting drift can change final redemption value.
|
|
24
26
|
- Hook/governor/deployer coupling: many bugs come from changing one layer while assuming the others are passive.
|
|
27
|
+
- Pending reserved supply and snapshot assumptions: settlement and quorum logic can drift if supply-sensitive views are taken at the wrong time.
|
|
28
|
+
- Scorecards that miss quorum do not naturally “finish.” New scorecards can still be submitted until no-contest logic takes over, so do not assume a clean defeated terminal state.
|
|
29
|
+
|
|
30
|
+
## Common Misdiagnoses
|
|
31
|
+
|
|
32
|
+
- A settlement bug is blamed on [`src/DefifaHook.sol`](../src/DefifaHook.sol) even though the wrong phase or grace-period state was created in [`src/DefifaDeployer.sol`](../src/DefifaDeployer.sol) or [`src/DefifaGovernor.sol`](../src/DefifaGovernor.sol).
|
|
33
|
+
- A governance bug is blamed on the governor even though attestation power or delegation semantics were wrong in the hook layer.
|
|
34
|
+
- An NFT-facing bug is blamed on settlement code even though the problem is resolver output in [`src/DefifaTokenUriResolver.sol`](../src/DefifaTokenUriResolver.sol).
|
|
35
|
+
- A Defifa-specific payout result is patched in this repo when the real bug is shared 721 or core protocol behavior upstream.
|
|
25
36
|
|
|
26
37
|
## Tests To Trust First
|
|
27
38
|
|
|
28
39
|
- [`test/DefifaGovernor.t.sol`](../test/DefifaGovernor.t.sol) for governance flow.
|
|
29
40
|
- [`test/DefifaNoContest.t.sol`](../test/DefifaNoContest.t.sol) for safety exits.
|
|
30
41
|
- [`test/DefifaFeeAccounting.t.sol`](../test/DefifaFeeAccounting.t.sol) and [`test/DefifaMintCostInvariant.t.sol`](../test/DefifaMintCostInvariant.t.sol) for economic correctness.
|
|
31
|
-
- [`test/DefifaHookRegressions.t.sol`](../test/DefifaHookRegressions.t.sol) and [`test/regression
|
|
32
|
-
- [`test/DefifaSecurity.t.sol`](../test/DefifaSecurity.t.sol)
|
|
42
|
+
- [`test/DefifaHookRegressions.t.sol`](../test/DefifaHookRegressions.t.sol), [`test/regression/GracePeriodBypass.t.sol`](../test/regression/GracePeriodBypass.t.sol), [`test/regression/FulfillmentBlocksRatification.t.sol`](../test/regression/FulfillmentBlocksRatification.t.sol), and [`test/regression/AttestationDelegateBeneficiary.t.sol`](../test/regression/AttestationDelegateBeneficiary.t.sol) for pinned regressions.
|
|
43
|
+
- [`test/DefifaSecurity.t.sol`](../test/DefifaSecurity.t.sol), [`test/DefifaGovernanceHardening.t.sol`](../test/DefifaGovernanceHardening.t.sol), [`test/DefifaAdversarialQuorum.t.sol`](../test/DefifaAdversarialQuorum.t.sol), and [`test/audit/`](../test/audit/) for adversarial and audit-derived cases.
|
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
|
|
@@ -394,6 +391,16 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
|
|
|
394
391
|
< block.timestamp + launchProjectData.refundPeriodDuration + launchProjectData.mintPeriodDuration
|
|
395
392
|
) revert DefifaDeployer_InvalidGameConfiguration();
|
|
396
393
|
|
|
394
|
+
// The hook and governor hardcode uint256[128] tier-weight tables, so reject games with more than 128 tiers.
|
|
395
|
+
if (launchProjectData.tiers.length > 128) revert DefifaDeployer_InvalidGameConfiguration();
|
|
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
|
+
|
|
397
404
|
// Get the game ID, optimistically knowing it will be one greater than the current count.
|
|
398
405
|
// Note: this prediction can race with other concurrent project deployments. If another project is
|
|
399
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
|
+
}
|