@ballkidz/defifa 0.0.2 → 0.0.4

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.
@@ -241,36 +241,6 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
241
241
  return string.concat(parts[0], Base64.encode(abi.encodePacked(parts[1], parts[2], parts[3])));
242
242
  }
243
243
 
244
- /// @notice Gets a substring.
245
- /// @dev If the first character is a space, it is not included.
246
- /// @param _str The string to get a substring of.
247
- /// @param _startIndex The first index of the substring from within the string.
248
- /// @param _endIndex The last index of the string from within the string.
249
- /// @return substring The substring.
250
- function _getSubstring(
251
- string memory _str,
252
- uint256 _startIndex,
253
- uint256 _endIndex
254
- )
255
- internal
256
- pure
257
- returns (string memory substring)
258
- {
259
- bytes memory _strBytes = bytes(_str);
260
- if (_startIndex >= _strBytes.length) return "";
261
- if (_endIndex > _strBytes.length) _endIndex = _strBytes.length;
262
- _startIndex = _strBytes[_startIndex] == bytes1(0x20) ? _startIndex + 1 : _startIndex;
263
- if (_startIndex >= _endIndex) return "";
264
- bytes memory _result = new bytes(_endIndex - _startIndex);
265
- for (uint256 _i = _startIndex; _i < _endIndex;) {
266
- _result[_i - _startIndex] = _strBytes[_i];
267
- unchecked {
268
- ++_i;
269
- }
270
- }
271
- return string(_result);
272
- }
273
-
274
244
  /// @notice Formats a balance from a fixed point number to a string.
275
245
  /// @param _amount The fixed point amount.
276
246
  /// @param _token The token the amount is in.
@@ -309,4 +279,34 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
309
279
  ? string(abi.encodePacked("\u039E", _integerPart, ".", _decimalPartStr))
310
280
  : string(abi.encodePacked(_integerPart, ".", _decimalPartStr, " ", IERC20Metadata(_token).symbol()));
311
281
  }
282
+
283
+ /// @notice Gets a substring.
284
+ /// @dev If the first character is a space, it is not included.
285
+ /// @param _str The string to get a substring of.
286
+ /// @param _startIndex The first index of the substring from within the string.
287
+ /// @param _endIndex The last index of the string from within the string.
288
+ /// @return substring The substring.
289
+ function _getSubstring(
290
+ string memory _str,
291
+ uint256 _startIndex,
292
+ uint256 _endIndex
293
+ )
294
+ internal
295
+ pure
296
+ returns (string memory substring)
297
+ {
298
+ bytes memory _strBytes = bytes(_str);
299
+ if (_startIndex >= _strBytes.length) return "";
300
+ if (_endIndex > _strBytes.length) _endIndex = _strBytes.length;
301
+ _startIndex = _strBytes[_startIndex] == bytes1(0x20) ? _startIndex + 1 : _startIndex;
302
+ if (_startIndex >= _endIndex) return "";
303
+ bytes memory _result = new bytes(_endIndex - _startIndex);
304
+ for (uint256 _i = _startIndex; _i < _endIndex;) {
305
+ _result[_i - _startIndex] = _strBytes[_i];
306
+ unchecked {
307
+ ++_i;
308
+ }
309
+ }
310
+ return string(_result);
311
+ }
312
312
  }
@@ -0,0 +1,374 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+
7
+ import {DefifaGovernor} from "../src/DefifaGovernor.sol";
8
+ import {DefifaDeployer} from "../src/DefifaDeployer.sol";
9
+ import {DefifaHook} from "../src/DefifaHook.sol";
10
+ import {DefifaTokenUriResolver} from "../src/DefifaTokenUriResolver.sol";
11
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
12
+
13
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
14
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
15
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
16
+ import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
17
+ import {
18
+ JB721TiersRulesetMetadataResolver
19
+ } from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
20
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
21
+
22
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
23
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
24
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
25
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
26
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
27
+ import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
28
+ import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
29
+ import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
30
+ import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
31
+
32
+ /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
33
+ contract TimestampReaderAudit {
34
+ function timestamp() external view returns (uint256) {
35
+ return block.timestamp;
36
+ }
37
+ }
38
+
39
+ /// @title DefifaHook_AuditFindings
40
+ /// @notice Regression tests for audit findings in DefifaHook.
41
+ contract DefifaHook_AuditFindings is JBTest, TestBaseWorkflow {
42
+ using JBRulesetMetadataResolver for JBRuleset;
43
+
44
+ TimestampReaderAudit private _tsReader = new TimestampReaderAudit();
45
+
46
+ address _protocolFeeProjectTokenAccount;
47
+ address _defifaProjectTokenAccount;
48
+ uint256 _protocolFeeProjectId;
49
+ uint256 _defifaProjectId;
50
+ uint256 _gameId = 3;
51
+
52
+ DefifaDeployer deployer;
53
+ DefifaHook hook;
54
+ DefifaGovernor governor;
55
+
56
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
57
+
58
+ function setUp() public virtual override {
59
+ super.setUp();
60
+
61
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
62
+ _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
63
+
64
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
65
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
66
+
67
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
68
+ rulesetConfigs[0] = JBRulesetConfig({
69
+ mustStartAtOrAfter: 0,
70
+ duration: 10 days,
71
+ weight: 1e18,
72
+ weightCutPercent: 0,
73
+ approvalHook: IJBRulesetApprovalHook(address(0)),
74
+ metadata: JBRulesetMetadata({
75
+ reservedPercent: 0,
76
+ cashOutTaxRate: 0,
77
+ baseCurrency: JBCurrencyIds.ETH,
78
+ pausePay: false,
79
+ pauseCreditTransfers: false,
80
+ allowOwnerMinting: false,
81
+ allowSetCustomToken: false,
82
+ allowTerminalMigration: false,
83
+ allowSetTerminals: false,
84
+ allowSetController: false,
85
+ allowAddAccountingContext: false,
86
+ allowAddPriceFeed: false,
87
+ ownerMustSendPayouts: false,
88
+ holdFees: false,
89
+ useTotalSurplusForCashOuts: false,
90
+ useDataHookForPay: true,
91
+ useDataHookForCashOut: true,
92
+ dataHook: address(0),
93
+ metadata: 0
94
+ }),
95
+ splitGroups: new JBSplitGroup[](0),
96
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
97
+ });
98
+
99
+ _protocolFeeProjectId =
100
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
101
+ vm.prank(projectOwner);
102
+ _protocolFeeProjectTokenAccount =
103
+ address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
104
+
105
+ _defifaProjectId =
106
+ jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
107
+ vm.prank(projectOwner);
108
+ _defifaProjectTokenAccount =
109
+ address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
110
+
111
+ hook = new DefifaHook(
112
+ jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
113
+ );
114
+ governor = new DefifaGovernor(jbController(), address(this));
115
+ JBAddressRegistry _registry = new JBAddressRegistry();
116
+ DefifaTokenUriResolver _tokenURIResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
117
+ deployer = new DefifaDeployer(
118
+ address(hook),
119
+ _tokenURIResolver,
120
+ governor,
121
+ jbController(),
122
+ _registry,
123
+ _defifaProjectId,
124
+ _protocolFeeProjectId
125
+ );
126
+
127
+ hook.transferOwnership(address(deployer));
128
+ governor.transferOwnership(address(deployer));
129
+ }
130
+
131
+ /// @notice Attestation units must be preserved when transferring an NFT to an undelegated recipient.
132
+ /// @dev Before the fix, transferring to a recipient with no delegate set would cause attestation units to vanish:
133
+ /// the sender's delegate lost units but no delegate gained them (because address(0) was skipped).
134
+ /// After the fix, the recipient auto-delegates to themselves, preserving total attestation power.
135
+ function test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient() public {
136
+ uint8 nTiers = 4;
137
+ address playerA = address(bytes20(keccak256("playerA")));
138
+ address playerB = address(bytes20(keccak256("playerB")));
139
+
140
+ DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(nTiers);
141
+ (uint256 _projectId, DefifaHook _nft,) = _createProject(defifaData);
142
+
143
+ // Phase 1: Mint — both players buy tier 1 NFTs.
144
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
145
+
146
+ // Player A mints tier 1.
147
+ vm.deal(playerA, 1 ether);
148
+ {
149
+ uint16[] memory rawMetadata = new uint16[](1);
150
+ rawMetadata[0] = 1;
151
+ bytes memory metadata = _buildPayMetadata(abi.encode(playerA, rawMetadata));
152
+ vm.prank(playerA);
153
+ jbMultiTerminal().pay{value: 1 ether}(
154
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, playerA, 0, "", metadata
155
+ );
156
+ }
157
+ assertEq(_nft.balanceOf(playerA), 1, "Player A should own 1 NFT");
158
+
159
+ // Player A explicitly sets delegation to self.
160
+ {
161
+ DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
162
+ delegations[0] = DefifaDelegation({delegatee: playerA, tierId: 1});
163
+ vm.prank(playerA);
164
+ _nft.setTierDelegatesTo(delegations);
165
+ }
166
+
167
+ // Player B mints tier 1 — uses self as attestation delegate (via pay metadata).
168
+ vm.deal(playerB, 1 ether);
169
+ {
170
+ uint16[] memory rawMetadata = new uint16[](1);
171
+ rawMetadata[0] = 1;
172
+ bytes memory metadata = _buildPayMetadata(abi.encode(playerB, rawMetadata));
173
+ vm.prank(playerB);
174
+ jbMultiTerminal().pay{value: 1 ether}(
175
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, playerB, 0, "", metadata
176
+ );
177
+ }
178
+ assertEq(_nft.balanceOf(playerB), 1, "Player B should own 1 NFT");
179
+
180
+ // Advance 1 second so checkpoints are recorded.
181
+ vm.warp(_tsReader.timestamp() + 1);
182
+
183
+ // Get the tier's voting units per NFT.
184
+ uint256 votingUnitsPerNft = _nft.store().tierOf(address(_nft), 1, false).votingUnits;
185
+ assertTrue(votingUnitsPerNft > 0, "Voting units should be non-zero");
186
+
187
+ // Capture the total tier attestation supply before transfer.
188
+ uint256 totalBefore = _nft.getTierTotalAttestationUnitsOf(1);
189
+ assertEq(totalBefore, votingUnitsPerNft * 2, "Total should be 2 NFTs worth of voting units");
190
+
191
+ // Verify Player A's delegate has attestation units.
192
+ uint256 playerADelegateUnitsBefore = _nft.getTierAttestationUnitsOf(playerA, 1);
193
+ assertEq(playerADelegateUnitsBefore, votingUnitsPerNft, "Player A delegate should have 1 NFT of voting units");
194
+
195
+ // Now create a NEW recipient (playerC) who has NEVER set delegation.
196
+ address playerC = address(bytes20(keccak256("playerC")));
197
+
198
+ // Warp to REFUND phase — delegation changes are locked (only MINT phase allows setTierDelegatesTo).
199
+ vm.warp(defifaData.start - defifaData.refundPeriodDuration);
200
+ // Confirm we are in REFUND phase by verifying setTierDelegatesTo reverts.
201
+ {
202
+ DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
203
+ delegations[0] = DefifaDelegation({delegatee: playerC, tierId: 1});
204
+ vm.prank(playerC);
205
+ vm.expectRevert(DefifaHook.DefifaHook_DelegateChangesUnavailableInThisPhase.selector);
206
+ _nft.setTierDelegatesTo(delegations);
207
+ }
208
+
209
+ // Player A transfers their NFT to playerC (who has no delegate set).
210
+ uint256 tokenId = _generateTokenId(1, 1); // Tier 1, token #1
211
+ vm.prank(playerA);
212
+ _nft.transferFrom(playerA, playerC, tokenId);
213
+
214
+ // Advance 1 second so checkpoints are recorded.
215
+ vm.warp(_tsReader.timestamp() + 1);
216
+
217
+ // After fix: playerC should be auto-delegated to themselves.
218
+ address playerCDelegate = _nft.getTierDelegateOf(playerC, 1);
219
+ assertEq(playerCDelegate, playerC, "Player C should be auto-delegated to self after receiving NFT");
220
+
221
+ // Player A's delegate should have lost the voting units.
222
+ uint256 playerADelegateUnitsAfter = _nft.getTierAttestationUnitsOf(playerA, 1);
223
+ assertEq(playerADelegateUnitsAfter, 0, "Player A delegate should have 0 voting units after transfer");
224
+
225
+ // Player C (auto-delegated to self) should have gained the voting units.
226
+ uint256 playerCDelegateUnits = _nft.getTierAttestationUnitsOf(playerC, 1);
227
+ assertEq(playerCDelegateUnits, votingUnitsPerNft, "Player C should have gained the transferred voting units");
228
+
229
+ // The total attestation supply should be unchanged — no units lost.
230
+ uint256 totalAfter = _nft.getTierTotalAttestationUnitsOf(1);
231
+ assertEq(totalAfter, totalBefore, "Total attestation units must be conserved across the transfer");
232
+
233
+ // Verify conservation: sum of all delegate attestation units for tier 1 == total.
234
+ uint256 sumOfDelegates = _nft.getTierAttestationUnitsOf(playerA, 1) + _nft.getTierAttestationUnitsOf(playerB, 1)
235
+ + _nft.getTierAttestationUnitsOf(playerC, 1);
236
+ assertEq(sumOfDelegates, totalAfter, "Sum of all delegate attestation units must equal total supply");
237
+ }
238
+
239
+ /// @notice Multiple sequential transfers to undelegated recipients should all preserve units.
240
+ function test_M5_multipleTransfersToUndelegatedRecipientsPreserveUnits() public {
241
+ uint8 nTiers = 2;
242
+ address playerA = address(bytes20(keccak256("playerA")));
243
+
244
+ DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(nTiers);
245
+ (uint256 _projectId, DefifaHook _nft,) = _createProject(defifaData);
246
+
247
+ // Phase 1: Mint
248
+ vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
249
+
250
+ // Player A mints tier 1.
251
+ vm.deal(playerA, 1 ether);
252
+ {
253
+ uint16[] memory rawMetadata = new uint16[](1);
254
+ rawMetadata[0] = 1;
255
+ bytes memory metadata = _buildPayMetadata(abi.encode(playerA, rawMetadata));
256
+ vm.prank(playerA);
257
+ jbMultiTerminal().pay{value: 1 ether}(
258
+ _projectId, JBConstants.NATIVE_TOKEN, 1 ether, playerA, 0, "", metadata
259
+ );
260
+ }
261
+
262
+ // Player A sets delegation.
263
+ {
264
+ DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
265
+ delegations[0] = DefifaDelegation({delegatee: playerA, tierId: 1});
266
+ vm.prank(playerA);
267
+ _nft.setTierDelegatesTo(delegations);
268
+ }
269
+
270
+ vm.warp(_tsReader.timestamp() + 1);
271
+
272
+ uint256 votingUnitsPerNft = _nft.store().tierOf(address(_nft), 1, false).votingUnits;
273
+ uint256 totalBefore = _nft.getTierTotalAttestationUnitsOf(1);
274
+ assertEq(totalBefore, votingUnitsPerNft, "Total should be 1 NFT worth of voting units");
275
+
276
+ // Warp to REFUND phase.
277
+ vm.warp(defifaData.start - defifaData.refundPeriodDuration);
278
+
279
+ uint256 tokenId = _generateTokenId(1, 1);
280
+
281
+ // Transfer through 3 undelegated recipients sequentially.
282
+ address currentHolder = playerA;
283
+ for (uint256 i = 0; i < 3; i++) {
284
+ address nextRecipient = address(uint160(0xBEEF0000 + i));
285
+
286
+ vm.prank(currentHolder);
287
+ _nft.transferFrom(currentHolder, nextRecipient, tokenId);
288
+ vm.warp(_tsReader.timestamp() + 1);
289
+
290
+ // Verify the recipient auto-delegated to self.
291
+ assertEq(
292
+ _nft.getTierDelegateOf(nextRecipient, 1), nextRecipient, "Each recipient should auto-delegate to self"
293
+ );
294
+
295
+ // Verify the recipient has the voting units.
296
+ assertEq(
297
+ _nft.getTierAttestationUnitsOf(nextRecipient, 1),
298
+ votingUnitsPerNft,
299
+ "Each recipient should hold the voting units"
300
+ );
301
+
302
+ // Verify total is conserved.
303
+ assertEq(
304
+ _nft.getTierTotalAttestationUnitsOf(1),
305
+ totalBefore,
306
+ "Total attestation units must remain constant across chain of transfers"
307
+ );
308
+
309
+ currentHolder = nextRecipient;
310
+ }
311
+ }
312
+
313
+ // ----- Internal helpers ------
314
+
315
+ function _getBasicLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
316
+ DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
317
+ for (uint256 i = 0; i < nTiers; i++) {
318
+ tierParams[i] = DefifaTierParams({
319
+ reservedRate: 1001,
320
+ reservedTokenBeneficiary: address(0),
321
+ encodedIPFSUri: bytes32(0),
322
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
323
+ name: "DEFIFA"
324
+ });
325
+ }
326
+
327
+ return DefifaLaunchProjectData({
328
+ name: "DEFIFA",
329
+ projectUri: "",
330
+ contractUri: "",
331
+ baseUri: "",
332
+ tierPrice: 1 ether,
333
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
334
+ mintPeriodDuration: 1 days,
335
+ start: uint48(block.timestamp + 3 days),
336
+ refundPeriodDuration: 1 days,
337
+ store: new JB721TiersHookStore(),
338
+ splits: new JBSplit[](0),
339
+ attestationStartTime: 0,
340
+ attestationGracePeriod: 100_381,
341
+ defaultAttestationDelegate: address(0),
342
+ tiers: tierParams,
343
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
344
+ terminal: jbMultiTerminal(),
345
+ minParticipation: 0,
346
+ scorecardTimeout: 0
347
+ });
348
+ }
349
+
350
+ function _createProject(DefifaLaunchProjectData memory defifaLaunchData)
351
+ internal
352
+ returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
353
+ {
354
+ _governor = governor;
355
+ (projectId) = deployer.launchGameWith(defifaLaunchData);
356
+ JBRuleset memory _fc = jbRulesets().currentOf(projectId);
357
+ if (_fc.dataHook() == address(0)) {
358
+ (_fc,) = jbRulesets().latestQueuedOf(projectId);
359
+ }
360
+ nft = DefifaHook(_fc.dataHook());
361
+ }
362
+
363
+ function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
364
+ return (_tierId * 1_000_000_000) + _tokenNumber;
365
+ }
366
+
367
+ function _buildPayMetadata(bytes memory metadata) internal returns (bytes memory) {
368
+ bytes[] memory data = new bytes[](1);
369
+ data[0] = metadata;
370
+ bytes4[] memory ids = new bytes4[](1);
371
+ ids[0] = metadataHelper().getId("pay", address(hook));
372
+ return metadataHelper().createMetadata(ids, data);
373
+ }
374
+ }