@ballkidz/defifa 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -13,13 +13,13 @@
13
13
  "url": "https://github.com/BallKidz/defifa-collection-deployer"
14
14
  },
15
15
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.32",
17
- "@bananapus/core-v6": "^0.0.32",
18
- "@bananapus/permission-ids-v6": "^0.0.15",
19
- "@croptop/core-v6": "^0.0.31",
16
+ "@bananapus/721-hook-v6": "^0.0.35",
17
+ "@bananapus/core-v6": "^0.0.34",
18
+ "@bananapus/permission-ids-v6": "^0.0.17",
19
+ "@croptop/core-v6": "^0.0.33",
20
20
  "@openzeppelin/contracts": "^5.6.1",
21
21
  "@prb/math": "^4.1.1",
22
- "@rev-net/core-v6": "^0.0.29",
22
+ "@rev-net/core-v6": "^0.0.32",
23
23
  "scripty.sol": "^2.1.1"
24
24
  },
25
25
  "devDependencies": {
@@ -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) is the deployment entry point when the task is about current wiring rather than game mechanics.
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.
@@ -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 conditions.
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/`](../test/regression/) for pinned regressions.
32
- - [`test/DefifaSecurity.t.sol`](../test/DefifaSecurity.t.sol) and [`test/DefifaGovernanceHardening.t.sol`](../test/DefifaGovernanceHardening.t.sol) for adversarial cases.
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.
@@ -394,6 +394,9 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
394
394
  < block.timestamp + launchProjectData.refundPeriodDuration + launchProjectData.mintPeriodDuration
395
395
  ) revert DefifaDeployer_InvalidGameConfiguration();
396
396
 
397
+ // The hook and governor hardcode uint256[128] tier-weight tables, so reject games with more than 128 tiers.
398
+ if (launchProjectData.tiers.length > 128) revert DefifaDeployer_InvalidGameConfiguration();
399
+
397
400
  // Get the game ID, optimistically knowing it will be one greater than the current count.
398
401
  // Note: this prediction can race with other concurrent project deployments. If another project is
399
402
  // created between reading count() and launchProjectFor(), the actual ID will differ. This is
@@ -249,6 +249,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
249
249
  /// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
250
250
  /// @return cashOutCount The amount of tokens that should be considered cashed out.
251
251
  /// @return totalSupply The total amount of tokens that are considered to be existing.
252
+ /// @return effectiveSurplusValue The effective surplus value to use for the cash out.
252
253
  /// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
253
254
  /// the beneficiary.
254
255
  function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
@@ -260,6 +261,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
260
261
  uint256 cashOutTaxRate,
261
262
  uint256 cashOutCount,
262
263
  uint256 totalSupply,
264
+ uint256 effectiveSurplusValue,
263
265
  JBCashOutHookSpecification[] memory hookSpecifications
264
266
  )
265
267
  {
@@ -304,6 +306,9 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
304
306
  // Use the surplus as the total supply.
305
307
  totalSupply = context.surplus.value;
306
308
 
309
+ // Use the surplus as the effective surplus value.
310
+ effectiveSurplusValue = context.surplus.value;
311
+
307
312
  // Use the cash out tax rate from the context.
308
313
  cashOutTaxRate = context.cashOutTaxRate;
309
314
  }
@@ -11,7 +11,6 @@ struct DefifaTierParams {
11
11
  string name;
12
12
  uint16 reservedRate;
13
13
  address reservedTokenBeneficiary;
14
- // forge-lint: disable-next-line(mixed-case-variable)
15
14
  bytes32 encodedIPFSUri;
16
15
  bool shouldUseReservedTokenBeneficiaryAsDefault;
17
16
  }
@@ -0,0 +1,171 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.28;
3
+
4
+ import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
5
+ import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
6
+ import {DefifaHook} from "../../src/DefifaHook.sol";
7
+ import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
8
+ import {DefifaGamePhase} from "../../src/enums/DefifaGamePhase.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
+
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 {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
16
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
17
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
18
+ import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
19
+ import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
20
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
21
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
22
+ import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
23
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
24
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
25
+ import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
26
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
27
+ import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
28
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
29
+ import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
30
+
31
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
32
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
33
+
34
+ contract CodexTierCapMismatchTest is JBTest, TestBaseWorkflow {
35
+ using JBRulesetMetadataResolver for JBRuleset;
36
+
37
+ DefifaDeployer internal deployer;
38
+ DefifaGovernor internal governor;
39
+ DefifaHook internal hookCodeOrigin;
40
+
41
+ function setUp() public virtual override {
42
+ super.setUp();
43
+
44
+ JBAccountingContext[] memory tokens = new JBAccountingContext[](1);
45
+ tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
46
+
47
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
48
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
49
+
50
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
51
+ rulesetConfigs[0] = JBRulesetConfig({
52
+ mustStartAtOrAfter: 0,
53
+ duration: 10 days,
54
+ weight: 1e18,
55
+ weightCutPercent: 0,
56
+ approvalHook: IJBRulesetApprovalHook(address(0)),
57
+ metadata: JBRulesetMetadata({
58
+ reservedPercent: 0,
59
+ cashOutTaxRate: 0,
60
+ baseCurrency: JBCurrencyIds.ETH,
61
+ pausePay: false,
62
+ pauseCreditTransfers: false,
63
+ allowOwnerMinting: false,
64
+ allowSetCustomToken: false,
65
+ allowTerminalMigration: false,
66
+ allowSetTerminals: false,
67
+ allowSetController: false,
68
+ allowAddAccountingContext: false,
69
+ allowAddPriceFeed: false,
70
+ ownerMustSendPayouts: false,
71
+ holdFees: false,
72
+ useTotalSurplusForCashOuts: false,
73
+ useDataHookForPay: true,
74
+ useDataHookForCashOut: true,
75
+ dataHook: address(0),
76
+ metadata: 0
77
+ }),
78
+ splitGroups: new JBSplitGroup[](0),
79
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
80
+ });
81
+
82
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
83
+ uint256 protocolFeeProjectId =
84
+ jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
85
+ vm.prank(projectOwner);
86
+ address nanaToken =
87
+ address(jbController().deployERC20For(protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
88
+
89
+ uint256 defifaProjectId = jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
90
+ vm.prank(projectOwner);
91
+ address defifaToken = address(jbController().deployERC20For(defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
92
+
93
+ hookCodeOrigin = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
94
+ governor = new DefifaGovernor(jbController(), address(this));
95
+ deployer = new DefifaDeployer(
96
+ address(hookCodeOrigin),
97
+ new DefifaTokenUriResolver(ITypeface(address(0))),
98
+ governor,
99
+ jbController(),
100
+ new JBAddressRegistry(),
101
+ defifaProjectId,
102
+ protocolFeeProjectId
103
+ );
104
+
105
+ hookCodeOrigin.transferOwnership(address(deployer));
106
+ governor.transferOwnership(address(deployer));
107
+ }
108
+
109
+ function test_launchRevertsFor129Tiers() external {
110
+ DefifaLaunchProjectData memory data = _launchData(129);
111
+
112
+ // H-5 fix: the deployer now caps tiers at 128, so launching with 129 reverts.
113
+ vm.expectRevert(DefifaDeployer.DefifaDeployer_InvalidGameConfiguration.selector);
114
+ deployer.launchGameWith(data);
115
+ }
116
+
117
+ function _launchData(uint256 tierCount) internal returns (DefifaLaunchProjectData memory) {
118
+ DefifaTierParams[] memory tiers = new DefifaTierParams[](tierCount);
119
+ for (uint256 i; i < tierCount; i++) {
120
+ tiers[i] = DefifaTierParams({
121
+ name: "TEAM",
122
+ reservedRate: 0,
123
+ reservedTokenBeneficiary: address(0),
124
+ encodedIPFSUri: bytes32(0),
125
+ shouldUseReservedTokenBeneficiaryAsDefault: false
126
+ });
127
+ }
128
+
129
+ return DefifaLaunchProjectData({
130
+ name: "DEFIFA",
131
+ projectUri: "",
132
+ contractUri: "",
133
+ baseUri: "",
134
+ tiers: tiers,
135
+ tierPrice: 1 ether,
136
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
137
+ mintPeriodDuration: 1 days,
138
+ refundPeriodDuration: 1 days,
139
+ start: uint48(block.timestamp + 3 days),
140
+ splits: new JBSplit[](0),
141
+ attestationStartTime: 0,
142
+ attestationGracePeriod: 100_381,
143
+ defaultAttestationDelegate: address(0),
144
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
145
+ terminal: jbMultiTerminal(),
146
+ store: new JB721TiersHookStore(),
147
+ minParticipation: 0,
148
+ scorecardTimeout: 7 days,
149
+ timelockDuration: 0
150
+ });
151
+ }
152
+
153
+ function _mintTier(uint256 gameId, uint16 tierId, uint256 amount) internal {
154
+ address buyer = address(bytes20(keccak256(abi.encodePacked("buyer", tierId))));
155
+ vm.deal(buyer, amount);
156
+
157
+ uint16[] memory tierIds = new uint16[](1);
158
+ tierIds[0] = tierId;
159
+
160
+ bytes[] memory payloads = new bytes[](1);
161
+ payloads[0] = abi.encode(address(0), tierIds);
162
+
163
+ bytes4[] memory ids = new bytes4[](1);
164
+ ids[0] = metadataHelper().getId("pay", address(hookCodeOrigin));
165
+
166
+ bytes memory metadata = metadataHelper().createMetadata(ids, payloads);
167
+
168
+ vm.prank(buyer);
169
+ jbMultiTerminal().pay{value: amount}(gameId, JBConstants.NATIVE_TOKEN, amount, buyer, 0, "", metadata);
170
+ }
171
+ }
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.28;
3
+
4
+ import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
5
+ import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
6
+ import {DefifaHook} from "../../src/DefifaHook.sol";
7
+ import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
8
+ import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
9
+ import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
10
+
11
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
12
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
13
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
14
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
15
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
16
+ import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
17
+ import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
18
+ import {JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
19
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
20
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
21
+ import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
22
+ import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
23
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
24
+ import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
25
+
26
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
27
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
28
+
29
+ /// @notice Tests for H-5 audit fix: tier cap of 128 enforced in launchGameWith().
30
+ contract H5TierCapValidationTest is JBTest, TestBaseWorkflow {
31
+ DefifaDeployer internal deployer;
32
+ DefifaGovernor internal governor;
33
+ DefifaHook internal hookCodeOrigin;
34
+
35
+ function setUp() public virtual override {
36
+ super.setUp();
37
+
38
+ JBAccountingContext[] memory tokens = new JBAccountingContext[](1);
39
+ tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
40
+
41
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
42
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
43
+
44
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
45
+ rulesetConfigs[0] = JBRulesetConfig({
46
+ mustStartAtOrAfter: 0,
47
+ duration: 10 days,
48
+ weight: 1e18,
49
+ weightCutPercent: 0,
50
+ approvalHook: IJBRulesetApprovalHook(address(0)),
51
+ metadata: JBRulesetMetadata({
52
+ reservedPercent: 0,
53
+ cashOutTaxRate: 0,
54
+ baseCurrency: JBCurrencyIds.ETH,
55
+ pausePay: false,
56
+ pauseCreditTransfers: false,
57
+ allowOwnerMinting: false,
58
+ allowSetCustomToken: false,
59
+ allowTerminalMigration: false,
60
+ allowSetTerminals: false,
61
+ allowSetController: false,
62
+ allowAddAccountingContext: false,
63
+ allowAddPriceFeed: false,
64
+ ownerMustSendPayouts: false,
65
+ holdFees: false,
66
+ useTotalSurplusForCashOuts: false,
67
+ useDataHookForPay: true,
68
+ useDataHookForCashOut: true,
69
+ dataHook: address(0),
70
+ metadata: 0
71
+ }),
72
+ splitGroups: new JBSplitGroup[](0),
73
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
74
+ });
75
+
76
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
77
+ uint256 protocolFeeProjectId =
78
+ jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
79
+ vm.prank(projectOwner);
80
+ address nanaToken =
81
+ address(jbController().deployERC20For(protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
82
+
83
+ uint256 defifaProjectId = jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
84
+ vm.prank(projectOwner);
85
+ address defifaToken = address(jbController().deployERC20For(defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
86
+
87
+ hookCodeOrigin = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
88
+ governor = new DefifaGovernor(jbController(), address(this));
89
+ deployer = new DefifaDeployer(
90
+ address(hookCodeOrigin),
91
+ new DefifaTokenUriResolver(ITypeface(address(0))),
92
+ governor,
93
+ jbController(),
94
+ new JBAddressRegistry(),
95
+ defifaProjectId,
96
+ protocolFeeProjectId
97
+ );
98
+
99
+ hookCodeOrigin.transferOwnership(address(deployer));
100
+ governor.transferOwnership(address(deployer));
101
+ }
102
+
103
+ /// @notice Launching with exactly 128 tiers should succeed (boundary).
104
+ function test_launch128TiersSucceeds() external {
105
+ DefifaLaunchProjectData memory data = _launchData(128);
106
+ uint256 gameId = deployer.launchGameWith(data);
107
+ assertGt(gameId, 0, "game should be created with 128 tiers");
108
+ }
109
+
110
+ /// @notice Launching with 129 tiers must revert with DefifaDeployer_InvalidGameConfiguration.
111
+ function test_launch129TiersReverts() external {
112
+ DefifaLaunchProjectData memory data = _launchData(129);
113
+ vm.expectRevert(DefifaDeployer.DefifaDeployer_InvalidGameConfiguration.selector);
114
+ deployer.launchGameWith(data);
115
+ }
116
+
117
+ /// @notice Launching with 1 tier should succeed (minimum valid).
118
+ function test_launch1TierSucceeds() external {
119
+ DefifaLaunchProjectData memory data = _launchData(1);
120
+ uint256 gameId = deployer.launchGameWith(data);
121
+ assertGt(gameId, 0, "game should be created with 1 tier");
122
+ }
123
+
124
+ /// @notice Launching with 0 tiers does not revert at the tier cap check (no lower-bound validation exists).
125
+ /// @dev This documents current behavior: the deployer only enforces the upper cap of 128.
126
+ function test_launch0TiersDoesNotRevertAtTierCap() external {
127
+ DefifaLaunchProjectData memory data = _launchData(0);
128
+ uint256 gameId = deployer.launchGameWith(data);
129
+ assertGt(gameId, 0, "0-tier game created (no lower-bound check)");
130
+ }
131
+
132
+ /// @notice Fuzz: any tier count above 128 reverts, any from 1-128 succeeds.
133
+ function test_fuzz_tierCapBoundary(uint256 tierCount) external {
134
+ tierCount = bound(tierCount, 1, 256);
135
+ DefifaLaunchProjectData memory data = _launchData(tierCount);
136
+
137
+ if (tierCount > 128) {
138
+ vm.expectRevert(DefifaDeployer.DefifaDeployer_InvalidGameConfiguration.selector);
139
+ deployer.launchGameWith(data);
140
+ } else {
141
+ uint256 gameId = deployer.launchGameWith(data);
142
+ assertGt(gameId, 0, "game should be created within tier cap");
143
+ }
144
+ }
145
+
146
+ // ─── Helpers
147
+ // ─────────────────────────────────────────────────────────────────
148
+
149
+ function _launchData(uint256 tierCount) internal returns (DefifaLaunchProjectData memory) {
150
+ DefifaTierParams[] memory tiers = new DefifaTierParams[](tierCount);
151
+ for (uint256 i; i < tierCount; i++) {
152
+ tiers[i] = DefifaTierParams({
153
+ name: "TEAM",
154
+ reservedRate: 0,
155
+ reservedTokenBeneficiary: address(0),
156
+ encodedIPFSUri: bytes32(0),
157
+ shouldUseReservedTokenBeneficiaryAsDefault: false
158
+ });
159
+ }
160
+
161
+ return DefifaLaunchProjectData({
162
+ name: "DEFIFA",
163
+ projectUri: "",
164
+ contractUri: "",
165
+ baseUri: "",
166
+ tiers: tiers,
167
+ tierPrice: 1 ether,
168
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
169
+ mintPeriodDuration: 1 days,
170
+ refundPeriodDuration: 1 days,
171
+ start: uint48(block.timestamp + 3 days),
172
+ splits: new JBSplit[](0),
173
+ attestationStartTime: 0,
174
+ attestationGracePeriod: 100_381,
175
+ defaultAttestationDelegate: address(0),
176
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
177
+ terminal: jbMultiTerminal(),
178
+ store: new JB721TiersHookStore(),
179
+ minParticipation: 0,
180
+ scorecardTimeout: 7 days,
181
+ timelockDuration: 0
182
+ });
183
+ }
184
+ }