@ballkidz/defifa 0.0.25 → 0.0.27

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.
Files changed (65) hide show
  1. package/AUDIT_INSTRUCTIONS.md +6 -2
  2. package/README.md +11 -2
  3. package/RISKS.md +3 -1
  4. package/STYLE_GUIDE.md +14 -11
  5. package/package.json +31 -14
  6. package/script/Deploy.s.sol +4 -1
  7. package/src/DefifaDeployer.sol +79 -47
  8. package/src/DefifaGovernor.sol +57 -12
  9. package/src/DefifaHook.sol +83 -26
  10. package/src/DefifaProjectOwner.sol +4 -3
  11. package/src/DefifaTokenUriResolver.sol +113 -20
  12. package/src/enums/DefifaGamePhase.sol +6 -0
  13. package/src/enums/DefifaScorecardState.sol +4 -0
  14. package/src/interfaces/IDefifaDeployer.sol +5 -0
  15. package/src/interfaces/IDefifaGamePhaseReporter.sol +4 -0
  16. package/src/interfaces/IDefifaGamePotReporter.sol +10 -0
  17. package/src/interfaces/IDefifaGovernor.sol +4 -0
  18. package/src/interfaces/IDefifaHook.sol +5 -0
  19. package/src/interfaces/IDefifaTokenUriResolver.sol +3 -0
  20. package/src/libraries/DefifaFontImporter.sol +1 -1
  21. package/src/libraries/DefifaHookLib.sol +9 -10
  22. package/src/structs/DefifaAttestations.sol +3 -2
  23. package/src/structs/DefifaDelegation.sol +1 -0
  24. package/src/structs/DefifaLaunchProjectData.sol +2 -3
  25. package/src/structs/DefifaOpsData.sol +1 -0
  26. package/src/structs/DefifaScorecard.sol +2 -0
  27. package/src/structs/DefifaTierCashOutWeight.sol +3 -1
  28. package/src/structs/DefifaTierParams.sol +1 -0
  29. package/CRYPTO_ECON.pdf +0 -0
  30. package/CRYPTO_ECON.tex +0 -997
  31. package/foundry.lock +0 -17
  32. package/references/operations.md +0 -32
  33. package/references/runtime.md +0 -43
  34. package/slither-ci.config.json +0 -10
  35. package/sphinx.lock +0 -521
  36. package/test/BWAFunctionComparison.t.sol +0 -1320
  37. package/test/DefifaAdversarialQuorum.t.sol +0 -617
  38. package/test/DefifaAuditLowGuards.t.sol +0 -308
  39. package/test/DefifaFeeAccounting.t.sol +0 -581
  40. package/test/DefifaGovernanceHardening.t.sol +0 -1315
  41. package/test/DefifaGovernor.t.sol +0 -1378
  42. package/test/DefifaHookRegressions.t.sol +0 -415
  43. package/test/DefifaMintCostInvariant.t.sol +0 -319
  44. package/test/DefifaNoContest.t.sol +0 -941
  45. package/test/DefifaSecurity.t.sol +0 -741
  46. package/test/DefifaUSDC.t.sol +0 -480
  47. package/test/Fork.t.sol +0 -2388
  48. package/test/TestAuditGaps.sol +0 -984
  49. package/test/TestQALastMile.t.sol +0 -514
  50. package/test/audit/AttestationDoubleCount.t.sol +0 -218
  51. package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
  52. package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
  53. package/test/audit/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
  54. package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
  55. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  56. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  57. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  58. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  59. package/test/audit/H5TierCapValidation.t.sol +0 -184
  60. package/test/audit/PendingReserveDilution.t.sol +0 -298
  61. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  62. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  63. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  64. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  65. package/test/regression/GracePeriodBypass.t.sol +0 -302
@@ -1,319 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
5
-
6
- import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
7
- import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
8
- import {DefifaHook} from "../../src/DefifaHook.sol";
9
- import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
10
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
11
- import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
12
-
13
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
14
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
15
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
16
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
17
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
18
- import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
19
- import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
20
- import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
21
- import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
22
- import {DefifaScorecardState} from "../../src/enums/DefifaScorecardState.sol";
23
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
24
- import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
25
- import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
26
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
27
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
28
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
29
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
30
- import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
31
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
32
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
33
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
34
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
35
-
36
- /// @notice Verifies that the pending-reserve snapshot fix prevents reserve minting from inflating
37
- /// attestation power after scorecard submission.
38
- contract PendingReserveSnapshotBypassTest is JBTest, TestBaseWorkflow {
39
- using JBRulesetMetadataResolver for JBRuleset;
40
-
41
- uint256 internal _protocolFeeProjectId;
42
- uint256 internal _defifaProjectId;
43
- uint256 internal _gameId = 3;
44
-
45
- DefifaDeployer internal _deployer;
46
- DefifaHook internal _hookImpl;
47
- DefifaGovernor internal _governorImpl;
48
-
49
- address internal _projectOwner = address(bytes20(keccak256("projectOwner")));
50
- address internal _reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
51
- address internal _player0 = address(bytes20(keccak256("player0")));
52
- address internal _player1 = address(bytes20(keccak256("player1")));
53
- address internal _player2 = address(bytes20(keccak256("player2")));
54
- address internal _player3 = address(bytes20(keccak256("player3")));
55
-
56
- DefifaHook internal _nft;
57
- DefifaGovernor internal _gov;
58
- uint256 internal _pid;
59
-
60
- function setUp() public virtual override {
61
- super.setUp();
62
-
63
- JBAccountingContext[] memory tokens = new JBAccountingContext[](1);
64
- tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
65
-
66
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
67
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
68
-
69
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
70
- rulesetConfigs[0] = JBRulesetConfig({
71
- mustStartAtOrAfter: 0,
72
- duration: 10 days,
73
- weight: 1e18,
74
- weightCutPercent: 0,
75
- approvalHook: IJBRulesetApprovalHook(address(0)),
76
- metadata: JBRulesetMetadata({
77
- reservedPercent: 0,
78
- cashOutTaxRate: 0,
79
- baseCurrency: JBCurrencyIds.ETH,
80
- pausePay: false,
81
- pauseCreditTransfers: false,
82
- allowOwnerMinting: false,
83
- allowSetCustomToken: false,
84
- allowTerminalMigration: false,
85
- allowSetTerminals: false,
86
- allowSetController: false,
87
- allowAddAccountingContext: false,
88
- allowAddPriceFeed: false,
89
- ownerMustSendPayouts: false,
90
- holdFees: false,
91
- useTotalSurplusForCashOuts: false,
92
- useDataHookForPay: true,
93
- useDataHookForCashOut: true,
94
- dataHook: address(0),
95
- metadata: 0
96
- }),
97
- splitGroups: new JBSplitGroup[](0),
98
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
99
- });
100
-
101
- _protocolFeeProjectId =
102
- jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
103
- vm.prank(_projectOwner);
104
- address nanaToken =
105
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
106
-
107
- _defifaProjectId =
108
- jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
109
- vm.prank(_projectOwner);
110
- address defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
111
-
112
- _hookImpl = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
113
- _governorImpl = new DefifaGovernor(jbController(), address(this));
114
- _deployer = new DefifaDeployer(
115
- address(_hookImpl),
116
- new DefifaTokenUriResolver(ITypeface(address(0))),
117
- _governorImpl,
118
- jbController(),
119
- new JBAddressRegistry(),
120
- _defifaProjectId,
121
- _protocolFeeProjectId
122
- );
123
-
124
- _hookImpl.transferOwnership(address(_deployer));
125
- _governorImpl.transferOwnership(address(_deployer));
126
- }
127
-
128
- /// @notice Confirms the snapshot fix: minting pending reserves after scorecard submission does NOT
129
- /// inflate the submitter's BWA attestation weight. The snapshot locks pending-reserve counts at
130
- /// submission time so that post-submission reserve minting cannot remove the dilution.
131
- function test_mintingPendingReserveAfterSnapshotInflatesVotingPowerAndFlipsOutcome() external {
132
- (_pid, _nft, _gov) = _launch(_launchData());
133
-
134
- vm.warp(block.timestamp + 1 days + 1);
135
- _mint(_player0, 1);
136
- _mint(_player1, 2);
137
- _mint(_player2, 3);
138
- _mint(_player3, 4);
139
- _delegateSelf(_player0, 1);
140
- _delegateSelf(_player1, 2);
141
- _delegateSelf(_player2, 3);
142
- _delegateSelf(_player3, 4);
143
-
144
- assertEq(_nft.store().numberOfPendingReservesFor(address(_nft), 1), 1, "tier 1 starts with pending reserve");
145
-
146
- vm.warp(block.timestamp + 2 days + 1);
147
-
148
- DefifaTierCashOutWeight[] memory scorecard = _evenScorecard();
149
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, scorecard);
150
- uint48 snapshotTime = uint48(block.timestamp);
151
-
152
- uint256 preBwa0 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, snapshotTime);
153
- uint256 preBwa1 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player1, snapshotTime);
154
-
155
- assertEq(preBwa0, 375_000_000, "pending reserve should dilute tier 1 holder at snapshot");
156
- assertEq(preBwa1, 750_000_000, "non-reserve tier holder keeps full post-BWA weight");
157
- assertEq(preBwa0 + preBwa1 + preBwa1, 1_875_000_000, "three attestors start below adjusted quorum");
158
-
159
- vm.warp(block.timestamp + 1);
160
-
161
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
162
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
163
- _nft.mintReservesFor(reserveConfigs);
164
-
165
- uint256 postRaw0 = _gov.getAttestationWeight(_gameId, _player0, snapshotTime);
166
- uint256 postBwa0 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, snapshotTime);
167
-
168
- // After fix: getAttestationWeight reads live state (reserves are now minted), so raw weight goes up.
169
- assertEq(postRaw0, 1_000_000_000, "minting reserves removes pending-reserve dilution in live view");
170
-
171
- // After fix: getBWAAttestationWeight uses the snapshot, so pending-reserve dilution is preserved.
172
- // The BWA weight does NOT double -- the snapshot prevents inflation.
173
- assertEq(postBwa0, 375_000_000, "snapshot prevents reserve minting from inflating BWA power");
174
-
175
- vm.startPrank(_player0);
176
- _gov.attestToScorecardFrom(_gameId, scorecardId);
177
- vm.stopPrank();
178
- vm.startPrank(_player1);
179
- _gov.attestToScorecardFrom(_gameId, scorecardId);
180
- vm.stopPrank();
181
- vm.startPrank(_player2);
182
- _gov.attestToScorecardFrom(_gameId, scorecardId);
183
- vm.stopPrank();
184
-
185
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
186
-
187
- // After fix: three attestors cannot reach quorum because the snapshot preserves the dilution.
188
- // Their combined weight: 375M + 750M + 750M = 1,875M < quorum (2,000M base).
189
- assertEq(
190
- uint256(_gov.stateOf(_gameId, scorecardId)),
191
- uint256(DefifaScorecardState.ACTIVE),
192
- "reserve mint after snapshot does NOT let three attestors reach quorum"
193
- );
194
- }
195
-
196
- /// @notice Pending reserve mints in the delayed-attestation window must not change BWA power.
197
- function test_mintingPendingReserveBeforeDelayedAttestationDoesNotChangeBWA() external {
198
- DefifaLaunchProjectData memory data = _launchData();
199
- data.attestationStartTime = uint48(block.timestamp + 5 days);
200
-
201
- (_pid, _nft, _gov) = _launch(data);
202
-
203
- vm.warp(block.timestamp + 1 days + 1);
204
- _mint(_player0, 1);
205
- _mint(_player1, 2);
206
- _mint(_player2, 3);
207
- _mint(_player3, 4);
208
- _delegateSelf(_player0, 1);
209
- _delegateSelf(_player1, 2);
210
- _delegateSelf(_player2, 3);
211
- _delegateSelf(_player3, 4);
212
-
213
- vm.warp(block.timestamp + 2 days);
214
-
215
- DefifaTierCashOutWeight[] memory scorecard = _evenScorecard();
216
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, scorecard);
217
- uint48 futureSnapshotTime = uint48(_gov.attestationStartTimeOf(_gameId) - 1);
218
-
219
- uint256 preRaw = _gov.getAttestationWeight(_gameId, _player0, futureSnapshotTime);
220
- uint256 preBwa = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, futureSnapshotTime);
221
-
222
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
223
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
224
- _nft.mintReservesFor(reserveConfigs);
225
-
226
- uint256 postRaw = _gov.getAttestationWeight(_gameId, _player0, futureSnapshotTime);
227
- uint256 postBwa = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, futureSnapshotTime);
228
-
229
- assertEq(preRaw, 500_000_000, "future raw snapshot includes the pending reserve exactly once");
230
- assertEq(preBwa, 375_000_000, "future BWA starts from the reserve-adjusted submission denominator");
231
- assertEq(postRaw, preRaw, "future raw power stays frozen before attestation begins");
232
- assertEq(postBwa, preBwa, "reserve mint in delayed window must not change BWA power");
233
- }
234
-
235
- function _evenScorecard() internal view returns (DefifaTierCashOutWeight[] memory scorecard) {
236
- scorecard = new DefifaTierCashOutWeight[](4);
237
- uint256 totalWeight = _nft.TOTAL_CASHOUT_WEIGHT();
238
- uint256 perTier = totalWeight / 4;
239
- for (uint256 i; i < 4; i++) {
240
- scorecard[i] = DefifaTierCashOutWeight({id: i + 1, cashOutWeight: perTier});
241
- }
242
- }
243
-
244
- function _launchData() internal returns (DefifaLaunchProjectData memory data) {
245
- DefifaTierParams[] memory tiers = new DefifaTierParams[](4);
246
- tiers[0] = DefifaTierParams({
247
- reservedRate: 1,
248
- reservedTokenBeneficiary: _reserveBeneficiary,
249
- encodedIPFSUri: bytes32(0),
250
- shouldUseReservedTokenBeneficiaryAsDefault: false,
251
- name: "TEAM"
252
- });
253
- for (uint256 i = 1; i < 4; i++) {
254
- tiers[i] = DefifaTierParams({
255
- reservedRate: 1001,
256
- reservedTokenBeneficiary: address(0),
257
- encodedIPFSUri: bytes32(0),
258
- shouldUseReservedTokenBeneficiaryAsDefault: false,
259
- name: "TEAM"
260
- });
261
- }
262
-
263
- data = DefifaLaunchProjectData({
264
- name: "DEFIFA",
265
- projectUri: "",
266
- contractUri: "",
267
- baseUri: "",
268
- tiers: tiers,
269
- tierPrice: 1 ether,
270
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
271
- mintPeriodDuration: 1 days,
272
- refundPeriodDuration: 1 days,
273
- start: uint48(block.timestamp + 3 days),
274
- splits: new JBSplit[](0),
275
- attestationStartTime: 0,
276
- attestationGracePeriod: 100_381,
277
- defaultAttestationDelegate: address(0),
278
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
279
- terminal: jbMultiTerminal(),
280
- store: new JB721TiersHookStore(),
281
- minParticipation: 0,
282
- scorecardTimeout: 0,
283
- timelockDuration: 0
284
- });
285
- }
286
-
287
- function _launch(DefifaLaunchProjectData memory data)
288
- internal
289
- returns (uint256 projectId, DefifaHook nft, DefifaGovernor gov)
290
- {
291
- gov = _governorImpl;
292
- projectId = _deployer.launchGameWith(data);
293
- JBRuleset memory ruleset = jbRulesets().currentOf(projectId);
294
- if (ruleset.dataHook() == address(0)) (ruleset,) = jbRulesets().latestQueuedOf(projectId);
295
- nft = DefifaHook(ruleset.dataHook());
296
- }
297
-
298
- function _mint(address user, uint256 tierId) internal {
299
- vm.deal(user, 1 ether);
300
- uint16[] memory tiers = new uint16[](1);
301
- // forge-lint: disable-next-line(unsafe-typecast)
302
- tiers[0] = uint16(tierId);
303
- bytes[] memory data = new bytes[](1);
304
- data[0] = abi.encode(user, tiers);
305
- bytes4[] memory ids = new bytes4[](1);
306
- ids[0] = metadataHelper().getId("pay", address(_hookImpl));
307
- bytes memory metadata = metadataHelper().createMetadata(ids, data);
308
-
309
- vm.prank(user);
310
- jbMultiTerminal().pay{value: 1 ether}(_pid, JBConstants.NATIVE_TOKEN, 1 ether, user, 0, "", metadata);
311
- }
312
-
313
- function _delegateSelf(address user, uint256 tierId) internal {
314
- DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
315
- delegations[0] = DefifaDelegation({delegatee: user, tierId: tierId});
316
- vm.prank(user);
317
- _nft.setTierDelegatesTo(delegations);
318
- }
319
- }
@@ -1,271 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
5
-
6
- import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
7
- import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
8
- import {DefifaHook} from "../../src/DefifaHook.sol";
9
- import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
10
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
11
-
12
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
13
- import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
14
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
15
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
16
-
17
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
18
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
19
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
20
- import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
21
- import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
22
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
23
- import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
24
- import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
25
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
26
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
27
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
28
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
29
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
30
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
31
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
32
-
33
- /// @title AttestationDelegateBeneficiary
34
- /// @notice Regression test for H-6: when payer != beneficiary and no explicit delegate is set,
35
- /// attestation delegation should default to the beneficiary (NFT recipient), not the payer.
36
- contract AttestationDelegateBeneficiary is JBTest, TestBaseWorkflow {
37
- using JBRulesetMetadataResolver for JBRuleset;
38
-
39
- uint256 _protocolFeeProjectId;
40
- uint256 _defifaProjectId;
41
- address projectOwner = address(bytes20(keccak256("projectOwner")));
42
-
43
- DefifaDeployer deployer;
44
- DefifaHook hook;
45
- DefifaGovernor governor;
46
-
47
- function setUp() public virtual override {
48
- super.setUp();
49
-
50
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
51
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
52
-
53
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
54
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
55
-
56
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
57
- rulesetConfigs[0] = JBRulesetConfig({
58
- mustStartAtOrAfter: 0,
59
- duration: 10 days,
60
- weight: 1e18,
61
- weightCutPercent: 0,
62
- approvalHook: IJBRulesetApprovalHook(address(0)),
63
- metadata: JBRulesetMetadata({
64
- reservedPercent: 0,
65
- cashOutTaxRate: 0,
66
- baseCurrency: JBCurrencyIds.ETH,
67
- pausePay: false,
68
- pauseCreditTransfers: false,
69
- allowOwnerMinting: false,
70
- allowSetCustomToken: false,
71
- allowTerminalMigration: false,
72
- allowSetTerminals: false,
73
- allowSetController: false,
74
- allowAddAccountingContext: false,
75
- allowAddPriceFeed: false,
76
- ownerMustSendPayouts: false,
77
- holdFees: false,
78
- useTotalSurplusForCashOuts: false,
79
- useDataHookForPay: true,
80
- useDataHookForCashOut: true,
81
- dataHook: address(0),
82
- metadata: 0
83
- }),
84
- splitGroups: new JBSplitGroup[](0),
85
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
86
- });
87
-
88
- _protocolFeeProjectId =
89
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
90
- vm.prank(projectOwner);
91
- address _nanaToken =
92
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
93
-
94
- _defifaProjectId =
95
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
96
- vm.prank(projectOwner);
97
- address _defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
98
-
99
- hook = new DefifaHook(jbDirectory(), IERC20(_defifaToken), IERC20(_nanaToken));
100
- governor = new DefifaGovernor(jbController(), address(this));
101
- deployer = new DefifaDeployer(
102
- address(hook),
103
- new DefifaTokenUriResolver(ITypeface(address(0))),
104
- governor,
105
- jbController(),
106
- new JBAddressRegistry(),
107
- _defifaProjectId,
108
- _protocolFeeProjectId
109
- );
110
-
111
- hook.transferOwnership(address(deployer));
112
- governor.transferOwnership(address(deployer));
113
- }
114
-
115
- /// @notice H-6: Default attestation delegate should be the beneficiary, not the payer.
116
- function test_defaultDelegateIsBeneficiaryNotPayer() public {
117
- address payer = address(bytes20(keccak256("payer")));
118
- address beneficiary = address(bytes20(keccak256("beneficiary")));
119
-
120
- // Launch game with no default attestation delegate.
121
- (uint256 _projectId, DefifaHook _nft) = _launchGame();
122
-
123
- // Warp to MINT phase.
124
- vm.warp(_mintPhaseStart);
125
-
126
- // Payer pays on behalf of beneficiary, no explicit delegate (address(0)).
127
- vm.deal(payer, 1 ether);
128
- uint16[] memory tierIds = new uint16[](1);
129
- tierIds[0] = 1;
130
- bytes memory payMetadata = abi.encode(address(0), tierIds); // attestationDelegate = address(0)
131
- bytes memory metadata = _buildPayMetadata(payMetadata);
132
-
133
- vm.prank(payer);
134
- jbMultiTerminal().pay{value: 1 ether}({
135
- projectId: _projectId,
136
- token: JBConstants.NATIVE_TOKEN,
137
- amount: 1 ether,
138
- beneficiary: beneficiary, // NFT goes to beneficiary, NOT payer
139
- minReturnedTokens: 0,
140
- memo: "",
141
- metadata: metadata
142
- });
143
-
144
- // H-6 fix: delegation should be on the beneficiary's account, not the payer's.
145
- // The beneficiary's delegate is themselves (default when no explicit delegate is set).
146
- address beneficiaryDelegate = _nft.getTierDelegateOf(beneficiary, 1);
147
- assertEq(beneficiaryDelegate, beneficiary, "H-6: default delegate should be beneficiary, not payer");
148
- // The payer should have no delegation since they didn't receive attestation units.
149
- address payerDelegate = _nft.getTierDelegateOf(payer, 1);
150
- assertEq(payerDelegate, address(0), "H-6: payer should have no delegation when payer != beneficiary");
151
- }
152
-
153
- /// @notice When payer == beneficiary, the default delegate should be that same address.
154
- function test_defaultDelegateIsSelfWhenPayerEqualsBeneficiary() public {
155
- address user = address(bytes20(keccak256("user")));
156
-
157
- (uint256 _projectId, DefifaHook _nft) = _launchGame();
158
- vm.warp(_mintPhaseStart);
159
-
160
- vm.deal(user, 1 ether);
161
- uint16[] memory tierIds = new uint16[](1);
162
- tierIds[0] = 1;
163
- bytes memory payMetadata = abi.encode(address(0), tierIds);
164
- bytes memory metadata = _buildPayMetadata(payMetadata);
165
-
166
- vm.prank(user);
167
- jbMultiTerminal().pay{value: 1 ether}({
168
- projectId: _projectId,
169
- token: JBConstants.NATIVE_TOKEN,
170
- amount: 1 ether,
171
- beneficiary: user, // payer == beneficiary
172
- minReturnedTokens: 0,
173
- memo: "",
174
- metadata: metadata
175
- });
176
-
177
- address delegate = _nft.getTierDelegateOf(user, 1);
178
- assertEq(delegate, user, "Default delegate should be self when payer == beneficiary");
179
- }
180
-
181
- /// @notice A third-party payer cannot override the beneficiary's delegate.
182
- function test_explicitDelegateFromThirdPartyDoesNotOverrideBeneficiaryDefault() public {
183
- address payer = address(bytes20(keccak256("payer2")));
184
- address beneficiary = address(bytes20(keccak256("beneficiary2")));
185
- address explicitDelegate = address(bytes20(keccak256("explicitDelegate")));
186
-
187
- (uint256 _projectId, DefifaHook _nft) = _launchGame();
188
- vm.warp(_mintPhaseStart);
189
-
190
- vm.deal(payer, 1 ether);
191
- uint16[] memory tierIds = new uint16[](1);
192
- tierIds[0] = 1;
193
- bytes memory payMetadata = abi.encode(explicitDelegate, tierIds);
194
- bytes memory metadata = _buildPayMetadata(payMetadata);
195
-
196
- vm.prank(payer);
197
- jbMultiTerminal().pay{value: 1 ether}({
198
- projectId: _projectId,
199
- token: JBConstants.NATIVE_TOKEN,
200
- amount: 1 ether,
201
- beneficiary: beneficiary,
202
- minReturnedTokens: 0,
203
- memo: "",
204
- metadata: metadata
205
- });
206
-
207
- address beneficiaryDelegate = _nft.getTierDelegateOf(beneficiary, 1);
208
- assertEq(beneficiaryDelegate, beneficiary, "third-party payer cannot overwrite beneficiary delegation");
209
- address payerDelegate = _nft.getTierDelegateOf(payer, 1);
210
- assertEq(payerDelegate, address(0), "Payer should have no delegation when payer != beneficiary");
211
- }
212
-
213
- // ----- Internal helpers ------
214
-
215
- /// @dev MINT phase starts at `start - mintPeriodDuration - refundPeriodDuration`.
216
- uint256 internal _mintPhaseStart;
217
-
218
- function _launchGame() internal returns (uint256 projectId, DefifaHook nft) {
219
- DefifaTierParams[] memory tierParams = new DefifaTierParams[](2);
220
- for (uint256 i = 0; i < 2; i++) {
221
- tierParams[i] = DefifaTierParams({
222
- reservedRate: 1001,
223
- reservedTokenBeneficiary: address(0),
224
- encodedIPFSUri: bytes32(0),
225
- shouldUseReservedTokenBeneficiaryAsDefault: false,
226
- name: "DEFIFA"
227
- });
228
- }
229
-
230
- DefifaLaunchProjectData memory d = DefifaLaunchProjectData({
231
- name: "DEFIFA",
232
- projectUri: "",
233
- contractUri: "",
234
- baseUri: "",
235
- tierPrice: 1 ether,
236
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
237
- mintPeriodDuration: 1 days,
238
- start: uint48(block.timestamp + 3 days),
239
- refundPeriodDuration: 1 days,
240
- store: new JB721TiersHookStore(),
241
- splits: new JBSplit[](0),
242
- attestationStartTime: 0,
243
- attestationGracePeriod: 100_381,
244
- defaultAttestationDelegate: address(0), // No default delegate -- should fall back to beneficiary
245
- tiers: tierParams,
246
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
247
- terminal: jbMultiTerminal(),
248
- minParticipation: 0,
249
- scorecardTimeout: 0,
250
- timelockDuration: 0
251
- });
252
-
253
- _mintPhaseStart = d.start - d.mintPeriodDuration - d.refundPeriodDuration;
254
-
255
- projectId = deployer.launchGameWith(d);
256
-
257
- JBRuleset memory fc = jbRulesets().currentOf(projectId);
258
- if (fc.dataHook() == address(0)) {
259
- (fc,) = jbRulesets().latestQueuedOf(projectId);
260
- }
261
- nft = DefifaHook(fc.dataHook());
262
- }
263
-
264
- function _buildPayMetadata(bytes memory metadata) internal view returns (bytes memory) {
265
- bytes[] memory data = new bytes[](1);
266
- data[0] = metadata;
267
- bytes4[] memory ids = new bytes4[](1);
268
- ids[0] = metadataHelper().getId("pay", address(hook));
269
- return metadataHelper().createMetadata(ids, data);
270
- }
271
- }