@ballkidz/defifa 0.0.25 → 0.0.26
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/AUDIT_INSTRUCTIONS.md +6 -2
- package/README.md +11 -2
- package/RISKS.md +3 -1
- package/STYLE_GUIDE.md +14 -11
- package/package.json +31 -14
- package/script/Deploy.s.sol +4 -1
- package/src/DefifaDeployer.sol +74 -46
- package/src/DefifaGovernor.sol +53 -11
- package/src/DefifaHook.sol +79 -25
- package/src/DefifaTokenUriResolver.sol +111 -19
- package/src/interfaces/IDefifaDeployer.sol +5 -0
- package/src/interfaces/IDefifaGovernor.sol +4 -0
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/src/libraries/DefifaHookLib.sol +9 -10
- package/src/structs/DefifaLaunchProjectData.sol +0 -3
- package/CRYPTO_ECON.pdf +0 -0
- package/CRYPTO_ECON.tex +0 -997
- package/foundry.lock +0 -17
- package/references/operations.md +0 -32
- package/references/runtime.md +0 -43
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -521
- package/test/BWAFunctionComparison.t.sol +0 -1320
- package/test/DefifaAdversarialQuorum.t.sol +0 -617
- package/test/DefifaAuditLowGuards.t.sol +0 -308
- package/test/DefifaFeeAccounting.t.sol +0 -581
- package/test/DefifaGovernanceHardening.t.sol +0 -1315
- package/test/DefifaGovernor.t.sol +0 -1378
- package/test/DefifaHookRegressions.t.sol +0 -415
- package/test/DefifaMintCostInvariant.t.sol +0 -319
- package/test/DefifaNoContest.t.sol +0 -941
- package/test/DefifaSecurity.t.sol +0 -741
- package/test/DefifaUSDC.t.sol +0 -480
- package/test/Fork.t.sol +0 -2388
- package/test/TestAuditGaps.sol +0 -984
- package/test/TestQALastMile.t.sol +0 -514
- package/test/audit/AttestationDoubleCount.t.sol +0 -218
- package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
- package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
- package/test/audit/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
- package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
- package/test/audit/CodexRegistryMismatch.t.sol +0 -191
- package/test/audit/CodexTierCapMismatch.t.sol +0 -171
- package/test/audit/CurrencyMismatchFix.t.sol +0 -265
- package/test/audit/FixPendingReserveDilution.t.sol +0 -366
- package/test/audit/H5TierCapValidation.t.sol +0 -184
- package/test/audit/PendingReserveDilution.t.sol +0 -298
- package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
- package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
- package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
- package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
- package/test/regression/GracePeriodBypass.t.sol +0 -302
package/src/DefifaGovernor.sol
CHANGED
|
@@ -31,6 +31,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
31
31
|
error DefifaGovernor_AlreadyRatified();
|
|
32
32
|
error DefifaGovernor_DuplicateScorecard();
|
|
33
33
|
error DefifaGovernor_GameNotFound();
|
|
34
|
+
error DefifaGovernor_GracePeriodTooShort();
|
|
34
35
|
error DefifaGovernor_IncorrectTierOrder();
|
|
35
36
|
error DefifaGovernor_NotAllowed();
|
|
36
37
|
error DefifaGovernor_NotAttested();
|
|
@@ -45,6 +46,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
45
46
|
/// @notice The max attestation power each tier has if every token within the tier attestations.
|
|
46
47
|
uint256 public constant override MAX_ATTESTATION_POWER_TIER = 1_000_000_000;
|
|
47
48
|
|
|
49
|
+
/// @notice The minimum attestation grace period enforced during game initialization.
|
|
50
|
+
uint256 public constant override MIN_ATTESTATION_GRACE_PERIOD = 1 days;
|
|
51
|
+
|
|
48
52
|
//*********************************************************************//
|
|
49
53
|
// --------------- public immutable stored properties ---------------- //
|
|
50
54
|
//*********************************************************************//
|
|
@@ -104,6 +108,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
104
108
|
/// @custom:param tierId The tier ID (0-indexed).
|
|
105
109
|
mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) internal _scorecardTierWeightsOf;
|
|
106
110
|
|
|
111
|
+
/// @notice The timestamp when quorum was first reached for a scorecard.
|
|
112
|
+
/// @dev Reset to 0 if attestations drop below quorum via revocation.
|
|
113
|
+
/// @custom:param gameId The ID of the game.
|
|
114
|
+
/// @custom:param scorecardId The ID of the scorecard.
|
|
115
|
+
mapping(uint256 => mapping(uint256 => uint48)) internal _quorumReachedAtOf;
|
|
116
|
+
|
|
107
117
|
//*********************************************************************//
|
|
108
118
|
// -------------------------- constructor ---------------------------- //
|
|
109
119
|
//*********************************************************************//
|
|
@@ -164,6 +174,11 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
164
174
|
// Increase the attestation count.
|
|
165
175
|
attestations.count += weight;
|
|
166
176
|
|
|
177
|
+
// Record when quorum is first reached so the timelock anchors to this moment.
|
|
178
|
+
if (_quorumReachedAtOf[gameId][scorecardId] == 0 && attestations.count >= scorecard.quorumSnapshot) {
|
|
179
|
+
_quorumReachedAtOf[gameId][scorecardId] = uint48(block.timestamp);
|
|
180
|
+
}
|
|
181
|
+
|
|
167
182
|
// Store the BWA weight that was added (used for accurate subtraction on revoke).
|
|
168
183
|
attestations.attestedWeightOf[msg.sender] = weight;
|
|
169
184
|
|
|
@@ -237,6 +252,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
237
252
|
attestations.count -= weight;
|
|
238
253
|
attestations.attestedWeightOf[msg.sender] = 0;
|
|
239
254
|
|
|
255
|
+
// Reset quorum timestamp if attestations drop below quorum.
|
|
256
|
+
DefifaScorecard storage scorecard = _scorecardOf[gameId][scorecardId];
|
|
257
|
+
if (attestations.count < scorecard.quorumSnapshot) {
|
|
258
|
+
_quorumReachedAtOf[gameId][scorecardId] = 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
240
261
|
emit AttestationRevoked(gameId, scorecardId, msg.sender, weight);
|
|
241
262
|
}
|
|
242
263
|
|
|
@@ -302,17 +323,24 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
302
323
|
if (scorecard.attestationsBegin != 0) revert DefifaGovernor_DuplicateScorecard();
|
|
303
324
|
|
|
304
325
|
uint256 attestationStartTime = attestationStartTimeOf(gameId);
|
|
326
|
+
|
|
327
|
+
// Game phase timing is timestamp-based by design.
|
|
328
|
+
uint256 currentTimestamp = block.timestamp;
|
|
305
329
|
uint256 timeUntilAttestationsBegin =
|
|
306
|
-
|
|
330
|
+
currentTimestamp > attestationStartTime ? 0 : attestationStartTime - currentTimestamp;
|
|
307
331
|
|
|
308
332
|
// Casting to uint48 is safe because block.timestamp fits in uint48 until year 8921556.
|
|
309
333
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
310
|
-
uint48 attestationsBegin = uint48(
|
|
334
|
+
uint48 attestationsBegin = uint48(currentTimestamp + timeUntilAttestationsBegin);
|
|
311
335
|
scorecard.attestationsBegin = attestationsBegin;
|
|
312
336
|
// Grace period extends from when attestations begin, not from submission time.
|
|
313
337
|
// This prevents the grace period from expiring before attestations even start
|
|
314
338
|
// when a scorecard is submitted early.
|
|
315
|
-
|
|
339
|
+
uint256 gracePeriodEnds = uint256(attestationsBegin) + attestationGracePeriodOf(gameId);
|
|
340
|
+
if (gracePeriodEnds > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
341
|
+
// Safe after the explicit max check above.
|
|
342
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
343
|
+
scorecard.gracePeriodEnds = uint48(gracePeriodEnds);
|
|
316
344
|
|
|
317
345
|
// Store tier weights for BWA computation.
|
|
318
346
|
for (uint256 i; i < numberOfTierWeights;) {
|
|
@@ -335,9 +363,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
335
363
|
// slither-disable-next-line calls-loop
|
|
336
364
|
JB721Tier memory tier =
|
|
337
365
|
hookStore.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
|
|
366
|
+
// Use adjusted pending reserves that account for refund-phase burns.
|
|
338
367
|
// slither-disable-next-line calls-loop
|
|
339
|
-
uint256 pendingReserves =
|
|
340
|
-
hookStore.numberOfPendingReservesFor({hook: metadata.dataHook, tierId: tierId});
|
|
368
|
+
uint256 pendingReserves = IDefifaHook(metadata.dataHook).adjustedPendingReservesFor(tierId);
|
|
341
369
|
// slither-disable-next-line calls-loop
|
|
342
370
|
uint256 submittedTierAttestationUnits =
|
|
343
371
|
IDefifaHook(metadata.dataHook).currentSupplyOfTier(tierId) * tier.votingUnits;
|
|
@@ -457,8 +485,8 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
457
485
|
// Set a default attestation start time if needed.
|
|
458
486
|
if (attestationStartTime == 0) attestationStartTime = block.timestamp;
|
|
459
487
|
|
|
460
|
-
// Enforce a minimum grace period
|
|
461
|
-
if (attestationGracePeriod <
|
|
488
|
+
// Enforce a minimum grace period to prevent instant ratification.
|
|
489
|
+
if (attestationGracePeriod < MIN_ATTESTATION_GRACE_PERIOD) revert DefifaGovernor_GracePeriodTooShort();
|
|
462
490
|
|
|
463
491
|
// Ensure values fit within their allocated 48-bit widths before packing.
|
|
464
492
|
if (attestationStartTime > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
@@ -554,8 +582,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
554
582
|
// When the reserve beneficiary later mints, their new NFTs add to the numerator while
|
|
555
583
|
// pending reserves decrease by the same amount — so no one's voting power shifts.
|
|
556
584
|
{
|
|
585
|
+
// Use adjusted pending reserves that account for refund-phase burns.
|
|
557
586
|
// slither-disable-next-line calls-loop
|
|
558
|
-
uint256 pendingReserves =
|
|
587
|
+
uint256 pendingReserves = IDefifaHook(metadata.dataHook).adjustedPendingReservesFor(tierId);
|
|
559
588
|
if (pendingReserves != 0) {
|
|
560
589
|
// slither-disable-next-line calls-loop
|
|
561
590
|
JB721Tier memory tier =
|
|
@@ -700,8 +729,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
700
729
|
// burner erase another participant's quorum contribution without erasing their claim.
|
|
701
730
|
// slither-disable-next-line calls-loop
|
|
702
731
|
uint256 currentTierSupply = hook.currentSupplyOfTier(tierId);
|
|
732
|
+
// Use adjusted pending reserves that account for refund-phase burns.
|
|
703
733
|
// slither-disable-next-line calls-loop
|
|
704
|
-
uint256 pendingReserves =
|
|
734
|
+
uint256 pendingReserves = hook.adjustedPendingReservesFor(tierId);
|
|
705
735
|
if (currentTierSupply != 0 || pendingReserves != 0) {
|
|
706
736
|
eligibleTierWeights += MAX_ATTESTATION_POWER_TIER;
|
|
707
737
|
}
|
|
@@ -744,12 +774,16 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
744
774
|
|
|
745
775
|
// If the scorecard has attestations beginning in the future, the state is PENDING.
|
|
746
776
|
// At exactly `attestationsBegin`, attestations are open so the state is ACTIVE.
|
|
777
|
+
// Game phase timing is timestamp-based by design.
|
|
778
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
747
779
|
if (scorecard.attestationsBegin > block.timestamp) {
|
|
748
780
|
return DefifaScorecardState.PENDING;
|
|
749
781
|
}
|
|
750
782
|
|
|
751
783
|
// If the scorecard's grace period has not yet ended, the state is ACTIVE.
|
|
752
784
|
// At exactly `gracePeriodEnds`, the grace period has elapsed so we fall through to the quorum check.
|
|
785
|
+
// Game phase timing is timestamp-based by design.
|
|
786
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
753
787
|
if (scorecard.gracePeriodEnds > block.timestamp) {
|
|
754
788
|
return DefifaScorecardState.ACTIVE;
|
|
755
789
|
}
|
|
@@ -757,8 +791,16 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
757
791
|
// If quorum has been reached (using the concentration-adjusted snapshot), check timelock.
|
|
758
792
|
if (scorecard.quorumSnapshot <= _scorecardAttestationsOf[gameId][scorecardId].count) {
|
|
759
793
|
uint256 timelockDur = timelockDurationOf(gameId);
|
|
760
|
-
if (timelockDur > 0
|
|
761
|
-
|
|
794
|
+
if (timelockDur > 0) {
|
|
795
|
+
// Anchor the timelock to the later of grace period end or when quorum was reached.
|
|
796
|
+
uint256 quorumReachedAt = _quorumReachedAtOf[gameId][scorecardId];
|
|
797
|
+
uint256 timelockAnchor =
|
|
798
|
+
quorumReachedAt > scorecard.gracePeriodEnds ? quorumReachedAt : uint256(scorecard.gracePeriodEnds);
|
|
799
|
+
// Game phase timing is timestamp-based by design.
|
|
800
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
801
|
+
if (block.timestamp < timelockAnchor + timelockDur) {
|
|
802
|
+
return DefifaScorecardState.QUEUED;
|
|
803
|
+
}
|
|
762
804
|
}
|
|
763
805
|
return DefifaScorecardState.SUCCEEDED;
|
|
764
806
|
}
|
package/src/DefifaHook.sol
CHANGED
|
@@ -45,6 +45,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
45
45
|
//*********************************************************************//
|
|
46
46
|
|
|
47
47
|
error DefifaHook_BadTierOrder();
|
|
48
|
+
error DefifaHook_IdenticalTokens();
|
|
48
49
|
error DefifaHook_DelegateAddressZero();
|
|
49
50
|
error DefifaHook_DelegateChangesUnavailableInThisPhase();
|
|
50
51
|
error DefifaHook_GameIsntScoringYet();
|
|
@@ -116,14 +117,23 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
116
117
|
// --------------------- public stored properties -------------------- //
|
|
117
118
|
//*********************************************************************//
|
|
118
119
|
|
|
120
|
+
/// @notice The amount that has been redeemed from this game, refunds are not counted.
|
|
121
|
+
uint256 public override amountRedeemed;
|
|
122
|
+
|
|
123
|
+
/// @notice The common base for the tokenUri's
|
|
124
|
+
string public override baseURI;
|
|
125
|
+
|
|
126
|
+
/// @notice A flag indicating if the cashout weights has been set.
|
|
127
|
+
bool public override cashOutWeightIsSet;
|
|
128
|
+
|
|
119
129
|
/// @notice The address of the origin 'DefifaHook', used to check in the init if the contract is the original or not
|
|
120
130
|
address public immutable override CODE_ORIGIN;
|
|
121
131
|
|
|
122
|
-
/// @notice
|
|
123
|
-
|
|
132
|
+
/// @notice Contract metadata uri.
|
|
133
|
+
string public override contractURI;
|
|
124
134
|
|
|
125
|
-
/// @notice The
|
|
126
|
-
|
|
135
|
+
/// @notice The address that'll be set as the attestation delegate by default.
|
|
136
|
+
address public override defaultAttestationDelegate;
|
|
127
137
|
|
|
128
138
|
/// @notice The contract reporting game phases.
|
|
129
139
|
IDefifaGamePhaseReporter public override gamePhaseReporter;
|
|
@@ -138,20 +148,16 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
138
148
|
/// @notice The currency that is accepted when minting tier NFTs.
|
|
139
149
|
uint256 public override pricingCurrency;
|
|
140
150
|
|
|
141
|
-
/// @notice
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
string public override baseURI;
|
|
146
|
-
|
|
147
|
-
/// @notice Contract metadata uri.
|
|
148
|
-
string public override contractURI;
|
|
151
|
+
/// @notice The number of tokens burned from a tier during non-COMPLETE phases (refund, no-contest).
|
|
152
|
+
/// @dev Used to adjust pending reserve counts so reserves that correspond to refunded mints
|
|
153
|
+
/// are excluded from the cash-out denominator.
|
|
154
|
+
mapping(uint256 => uint256) public refundedBurnsFrom;
|
|
149
155
|
|
|
150
|
-
/// @notice The
|
|
151
|
-
|
|
156
|
+
/// @notice The contract storing all funding cycle configurations.
|
|
157
|
+
IJBRulesets public override rulesets;
|
|
152
158
|
|
|
153
|
-
/// @notice The
|
|
154
|
-
|
|
159
|
+
/// @notice The contract that stores and manages the NFT's data.
|
|
160
|
+
IJB721TiersHookStore public override store;
|
|
155
161
|
|
|
156
162
|
/// @notice The amount of tokens that have been redeemed from a tier, refunds are not counted.
|
|
157
163
|
/// @custom:param The tier from which tokens have been redeemed.
|
|
@@ -161,6 +167,41 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
161
167
|
// ------------------------- external views -------------------------- //
|
|
162
168
|
//*********************************************************************//
|
|
163
169
|
|
|
170
|
+
/// @notice Returns the adjusted pending reserve count for a tier, accounting for refund-phase burns.
|
|
171
|
+
/// @dev Recalculates reserves from (paidMints - burns) / reserveFrequency since the relationship
|
|
172
|
+
/// between burns and reserves is not linear — it depends on the tier's reserve frequency.
|
|
173
|
+
/// @param tierId The tier ID.
|
|
174
|
+
/// @return The adjusted pending reserve count (floored at 0).
|
|
175
|
+
// slither-disable-next-line calls-loop
|
|
176
|
+
function adjustedPendingReservesFor(uint256 tierId) public view returns (uint256) {
|
|
177
|
+
uint256 refundBurns = refundedBurnsFrom[tierId];
|
|
178
|
+
|
|
179
|
+
// If no refund burns, return the store's value directly.
|
|
180
|
+
if (refundBurns == 0) return store.numberOfPendingReservesFor({hook: address(this), tierId: tierId});
|
|
181
|
+
|
|
182
|
+
// Get the tier to access reserveFrequency and supply data.
|
|
183
|
+
JB721Tier memory tier = store.tierOf({hook: address(this), id: tierId, includeResolvedUri: false});
|
|
184
|
+
|
|
185
|
+
// No reserves if no reserve frequency.
|
|
186
|
+
if (tier.reserveFrequency == 0) return 0;
|
|
187
|
+
|
|
188
|
+
// Calculate the number of reserves already minted.
|
|
189
|
+
uint256 reservesMinted = store.numberOfReservesMintedFor({hook: address(this), tierId: tierId});
|
|
190
|
+
|
|
191
|
+
// Calculate non-reserve mints: initialSupply - remainingSupply - reservesMinted.
|
|
192
|
+
uint256 nonReserveMints = tier.initialSupply - tier.remainingSupply - reservesMinted;
|
|
193
|
+
|
|
194
|
+
// Subtract refund burns from non-reserve mints (burns can't exceed non-reserve mints).
|
|
195
|
+
uint256 adjustedMints = nonReserveMints > refundBurns ? nonReserveMints - refundBurns : 0;
|
|
196
|
+
|
|
197
|
+
// Recalculate available reserves: ceil(adjustedMints / reserveFrequency).
|
|
198
|
+
uint256 availableReserves = adjustedMints / tier.reserveFrequency;
|
|
199
|
+
if (adjustedMints % tier.reserveFrequency > 0) ++availableReserves;
|
|
200
|
+
|
|
201
|
+
// Return pending = available - already minted (floored at 0).
|
|
202
|
+
return availableReserves > reservesMinted ? availableReserves - reservesMinted : 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
164
205
|
/// @notice The first owner of each token ID, which corresponds to the address that originally contributed to the
|
|
165
206
|
/// project to receive the NFT.
|
|
166
207
|
/// @param tokenId The ID of the token to get the first owner of.
|
|
@@ -304,8 +345,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
304
345
|
// slither-disable-next-line calls-loop
|
|
305
346
|
cumulativeMintPrice -= hookStore.tierOfTokenId({
|
|
306
347
|
hook: address(this), tokenId: decodedTokenIds[i], includeResolvedUri: false
|
|
307
|
-
})
|
|
308
|
-
.price;
|
|
348
|
+
}).price;
|
|
309
349
|
}
|
|
310
350
|
|
|
311
351
|
unchecked {
|
|
@@ -418,12 +458,15 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
418
458
|
// If the game isn't complete, we do not have any tokens to claim.
|
|
419
459
|
if (gamePhaseReporter.currentGamePhaseOf(PROJECT_ID) != DefifaGamePhase.COMPLETE) return (0, 0);
|
|
420
460
|
|
|
461
|
+
// Include unminted reserves in the denominator. Once reserves are pending, their future recipients are
|
|
462
|
+
// entitled to fee-token claims as if the reserve NFTs had already been minted; otherwise paid holders could
|
|
463
|
+
// claim too large a share before the reserve mint transaction lands.
|
|
421
464
|
// slither-disable-next-line unused-return
|
|
422
465
|
return DefifaHookLib.computeTokensClaim({
|
|
423
466
|
tokenIds: tokenIds,
|
|
424
467
|
hookStore: store,
|
|
425
468
|
hook: address(this),
|
|
426
|
-
totalMintCost: _totalMintCost,
|
|
469
|
+
totalMintCost: _totalMintCost + _pendingReserveMintCost(),
|
|
427
470
|
defifaBalance: DEFIFA_TOKEN.balanceOf(address(this)),
|
|
428
471
|
baseProtocolBalance: BASE_PROTOCOL_TOKEN.balanceOf(address(this))
|
|
429
472
|
});
|
|
@@ -449,6 +492,8 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
449
492
|
JB721Hook(_directory)
|
|
450
493
|
Ownable(msg.sender)
|
|
451
494
|
{
|
|
495
|
+
if (address(_defifaToken) == address(_baseProtocolToken)) revert DefifaHook_IdenticalTokens();
|
|
496
|
+
|
|
452
497
|
CODE_ORIGIN = address(this);
|
|
453
498
|
DEFIFA_TOKEN = _defifaToken;
|
|
454
499
|
BASE_PROTOCOL_TOKEN = _baseProtocolToken;
|
|
@@ -574,6 +619,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
574
619
|
/// @notice Mint reserved tokens within the tier for the provided value.
|
|
575
620
|
/// @param tierId The ID of the tier to mint within.
|
|
576
621
|
/// @param count The number of reserved tokens to mint.
|
|
622
|
+
// slither-disable-next-line reentrancy-benign,reentrancy-no-eth
|
|
577
623
|
function mintReservesFor(uint256 tierId, uint256 count) public override {
|
|
578
624
|
// Minting reserves must not be paused.
|
|
579
625
|
// slither-disable-next-line calls-loop
|
|
@@ -703,13 +749,20 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
703
749
|
}
|
|
704
750
|
|
|
705
751
|
// Burn the token.
|
|
752
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
706
753
|
_burn(tokenId);
|
|
707
754
|
|
|
708
|
-
//
|
|
755
|
+
// slither-disable-next-line calls-loop
|
|
756
|
+
uint256 tierId = hookStore.tierIdOfToken(tokenId);
|
|
709
757
|
if (isComplete) {
|
|
758
|
+
// Track per-tier redemptions during the complete phase.
|
|
759
|
+
unchecked {
|
|
760
|
+
++tokensRedeemedFrom[tierId];
|
|
761
|
+
}
|
|
762
|
+
} else {
|
|
763
|
+
// Track non-COMPLETE burns (refund/no-contest) so pending reserve counts can be adjusted.
|
|
710
764
|
unchecked {
|
|
711
|
-
|
|
712
|
-
++tokensRedeemedFrom[hookStore.tierIdOfToken(tokenId)];
|
|
765
|
+
++refundedBurnsFrom[tierId];
|
|
713
766
|
}
|
|
714
767
|
}
|
|
715
768
|
|
|
@@ -735,7 +788,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
735
788
|
// are accounted for, preventing paid holders from claiming a disproportionate share.
|
|
736
789
|
// slither-disable-next-line reentrancy-events
|
|
737
790
|
beneficiaryReceivedTokens = _claimTokensFor({
|
|
738
|
-
beneficiary: context.
|
|
791
|
+
beneficiary: context.beneficiary,
|
|
739
792
|
shareToBeneficiary: cumulativeMintPrice,
|
|
740
793
|
outOfTotal: _totalMintCost + _pendingReserveMintCost()
|
|
741
794
|
});
|
|
@@ -854,11 +907,12 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
854
907
|
|
|
855
908
|
for (uint256 i; i < numberOfTiers;) {
|
|
856
909
|
uint256 tierId = i + 1;
|
|
857
|
-
|
|
858
|
-
uint256 pendingReserves = hookStore.numberOfPendingReservesFor({hook: address(this), tierId: tierId});
|
|
910
|
+
uint256 pendingReserves = adjustedPendingReservesFor(tierId);
|
|
859
911
|
if (pendingReserves != 0) {
|
|
860
912
|
// slither-disable-next-line calls-loop
|
|
861
913
|
JB721Tier memory tier = hookStore.tierOf({hook: address(this), id: tierId, includeResolvedUri: false});
|
|
914
|
+
|
|
915
|
+
// Pending reserves dilute claims by the same economic weight as paid mints at this tier's price.
|
|
862
916
|
cost += pendingReserves * tier.price;
|
|
863
917
|
}
|
|
864
918
|
unchecked {
|
|
@@ -70,12 +70,13 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
70
70
|
// Keep a reference to the rarity text;
|
|
71
71
|
string memory valueText;
|
|
72
72
|
|
|
73
|
-
// Keep a reference to the game's name.
|
|
73
|
+
// Keep a reference to the game's name (escaped for JSON/SVG safety).
|
|
74
74
|
// TODO: Somehow make the `IDefifaHook` have the `name` function.
|
|
75
|
-
string memory
|
|
75
|
+
string memory titleSvg = _escapeSvg(ERC721(address(hook)).name());
|
|
76
76
|
|
|
77
77
|
// Keep a reference to the tier's name.
|
|
78
|
-
string memory
|
|
78
|
+
string memory teamJson;
|
|
79
|
+
string memory teamSvg;
|
|
79
80
|
|
|
80
81
|
// Keep a reference to the SVG parts.
|
|
81
82
|
string[] memory parts = new string[](4);
|
|
@@ -88,8 +89,10 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
88
89
|
JB721Tier memory tier =
|
|
89
90
|
hook.store().tierOfTokenId({hook: address(hook), tokenId: tokenId, includeResolvedUri: false});
|
|
90
91
|
|
|
91
|
-
// Set the tier's name.
|
|
92
|
-
|
|
92
|
+
// Set the tier's name (escaped for JSON/SVG safety).
|
|
93
|
+
string memory rawTeam = hook.tierNameOf(tier.id);
|
|
94
|
+
teamJson = _escapeJson(rawTeam);
|
|
95
|
+
teamSvg = _escapeSvg(rawTeam);
|
|
93
96
|
|
|
94
97
|
// Check to see if the tier has a URI. Return it if it does.
|
|
95
98
|
if (tier.encodedIPFSUri != bytes32(0)) {
|
|
@@ -101,11 +104,11 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
101
104
|
parts[1] = string(
|
|
102
105
|
abi.encodePacked(
|
|
103
106
|
'{"name":"',
|
|
104
|
-
|
|
107
|
+
teamJson,
|
|
105
108
|
'", "id": "',
|
|
106
109
|
uint256(tier.id).toString(),
|
|
107
110
|
'","description":"Team: ',
|
|
108
|
-
|
|
111
|
+
teamJson,
|
|
109
112
|
", ID: ",
|
|
110
113
|
uint256(tier.id).toString(),
|
|
111
114
|
'.","image":"data:image/svg+xml;base64,'
|
|
@@ -201,27 +204,27 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
201
204
|
gamePhaseText,
|
|
202
205
|
"</text>",
|
|
203
206
|
'<text x="10" y="85" style="font-size:26px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">',
|
|
204
|
-
_getSubstring(
|
|
207
|
+
_getSubstring(titleSvg, 0, 30),
|
|
205
208
|
"</text>",
|
|
206
209
|
'<text x="10" y="120" style="font-size:26px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">',
|
|
207
|
-
_getSubstring(
|
|
210
|
+
_getSubstring(titleSvg, 30, 60),
|
|
208
211
|
"</text>",
|
|
209
212
|
'<text x="10" y="205" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
|
|
210
|
-
bytes(_getSubstring(
|
|
211
|
-
? _getSubstring(
|
|
213
|
+
bytes(_getSubstring(teamSvg, 20, 30)).length != 0 && bytes(_getSubstring(teamSvg, 10, 20)).length != 0
|
|
214
|
+
? _getSubstring(teamSvg, 0, 10)
|
|
212
215
|
: "",
|
|
213
216
|
"</text>",
|
|
214
217
|
'<text x="10" y="295" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
|
|
215
|
-
bytes(_getSubstring(
|
|
216
|
-
? _getSubstring(
|
|
217
|
-
: bytes(_getSubstring(
|
|
218
|
+
bytes(_getSubstring(teamSvg, 20, 30)).length != 0
|
|
219
|
+
? _getSubstring(teamSvg, 10, 20)
|
|
220
|
+
: bytes(_getSubstring(teamSvg, 10, 20)).length != 0 ? _getSubstring(teamSvg, 0, 10) : "",
|
|
218
221
|
"</text>",
|
|
219
222
|
'<text x="10" y="385" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
|
|
220
|
-
bytes(_getSubstring(
|
|
221
|
-
? _getSubstring(
|
|
222
|
-
: bytes(_getSubstring(
|
|
223
|
-
? _getSubstring(
|
|
224
|
-
: _getSubstring(
|
|
223
|
+
bytes(_getSubstring(teamSvg, 20, 30)).length != 0
|
|
224
|
+
? _getSubstring(teamSvg, 20, 30)
|
|
225
|
+
: bytes(_getSubstring(teamSvg, 10, 20)).length != 0
|
|
226
|
+
? _getSubstring(teamSvg, 10, 20)
|
|
227
|
+
: _getSubstring(teamSvg, 0, 10),
|
|
225
228
|
"</text>",
|
|
226
229
|
'<text x="10" y="430" style="font-size:16px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">TOKEN ID: ',
|
|
227
230
|
tokenId.toString(),
|
|
@@ -312,4 +315,93 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
312
315
|
}
|
|
313
316
|
return string(result);
|
|
314
317
|
}
|
|
318
|
+
|
|
319
|
+
/// @notice Escapes special characters for safe JSON string interpolation.
|
|
320
|
+
/// @param input The raw string.
|
|
321
|
+
/// @return The escaped string.
|
|
322
|
+
function _escapeJson(string memory input) internal pure returns (string memory) {
|
|
323
|
+
bytes memory b = bytes(input);
|
|
324
|
+
// Worst case: every char needs escaping (2x length).
|
|
325
|
+
bytes memory out = new bytes(b.length * 2);
|
|
326
|
+
uint256 j;
|
|
327
|
+
for (uint256 i; i < b.length;) {
|
|
328
|
+
bytes1 c = b[i];
|
|
329
|
+
if (c == '"' || c == "\\") {
|
|
330
|
+
out[j++] = "\\";
|
|
331
|
+
out[j++] = c;
|
|
332
|
+
} else if (c == 0x0a) {
|
|
333
|
+
// newline
|
|
334
|
+
out[j++] = "\\";
|
|
335
|
+
out[j++] = "n";
|
|
336
|
+
} else if (c == 0x0d) {
|
|
337
|
+
// carriage return
|
|
338
|
+
out[j++] = "\\";
|
|
339
|
+
out[j++] = "r";
|
|
340
|
+
} else {
|
|
341
|
+
out[j++] = c;
|
|
342
|
+
}
|
|
343
|
+
unchecked {
|
|
344
|
+
++i;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Trim to actual length.
|
|
348
|
+
bytes memory trimmed = new bytes(j);
|
|
349
|
+
for (uint256 k; k < j;) {
|
|
350
|
+
trimmed[k] = out[k];
|
|
351
|
+
unchecked {
|
|
352
|
+
++k;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return string(trimmed);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// @notice Escapes special characters for safe SVG text interpolation.
|
|
359
|
+
/// @param input The raw string.
|
|
360
|
+
/// @return The escaped string.
|
|
361
|
+
function _escapeSvg(string memory input) internal pure returns (string memory) {
|
|
362
|
+
bytes memory b = bytes(input);
|
|
363
|
+
// Worst case: each char becomes 5 chars (&).
|
|
364
|
+
bytes memory out = new bytes(b.length * 5);
|
|
365
|
+
uint256 j;
|
|
366
|
+
for (uint256 i; i < b.length;) {
|
|
367
|
+
bytes1 c = b[i];
|
|
368
|
+
if (c == "&") {
|
|
369
|
+
out[j++] = "&";
|
|
370
|
+
out[j++] = "a";
|
|
371
|
+
out[j++] = "m";
|
|
372
|
+
out[j++] = "p";
|
|
373
|
+
out[j++] = ";";
|
|
374
|
+
} else if (c == "<") {
|
|
375
|
+
out[j++] = "&";
|
|
376
|
+
out[j++] = "l";
|
|
377
|
+
out[j++] = "t";
|
|
378
|
+
out[j++] = ";";
|
|
379
|
+
} else if (c == ">") {
|
|
380
|
+
out[j++] = "&";
|
|
381
|
+
out[j++] = "g";
|
|
382
|
+
out[j++] = "t";
|
|
383
|
+
out[j++] = ";";
|
|
384
|
+
} else if (c == '"') {
|
|
385
|
+
out[j++] = "&";
|
|
386
|
+
out[j++] = "q";
|
|
387
|
+
out[j++] = "u";
|
|
388
|
+
out[j++] = "o";
|
|
389
|
+
out[j++] = "t";
|
|
390
|
+
out[j++] = ";";
|
|
391
|
+
} else {
|
|
392
|
+
out[j++] = c;
|
|
393
|
+
}
|
|
394
|
+
unchecked {
|
|
395
|
+
++i;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
bytes memory trimmed = new bytes(j);
|
|
399
|
+
for (uint256 k; k < j;) {
|
|
400
|
+
trimmed[k] = out[k];
|
|
401
|
+
unchecked {
|
|
402
|
+
++k;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return string(trimmed);
|
|
406
|
+
}
|
|
315
407
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
4
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
5
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
5
6
|
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
6
7
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
7
8
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
@@ -88,6 +89,10 @@ interface IDefifaDeployer {
|
|
|
88
89
|
/// @return The code origin address.
|
|
89
90
|
function HOOK_CODE_ORIGIN() external view returns (address);
|
|
90
91
|
|
|
92
|
+
/// @notice The 721 tiers hook store used by all games.
|
|
93
|
+
/// @return The hook store contract.
|
|
94
|
+
function HOOK_STORE() external view returns (IJB721TiersHookStore);
|
|
95
|
+
|
|
91
96
|
/// @notice The address registry used for content-addressable deployment lookups.
|
|
92
97
|
/// @return The address registry contract.
|
|
93
98
|
function REGISTRY() external view returns (IJBAddressRegistry);
|
|
@@ -123,6 +123,10 @@ interface IDefifaGovernor {
|
|
|
123
123
|
/// @return The maximum attestation power tier.
|
|
124
124
|
function MAX_ATTESTATION_POWER_TIER() external view returns (uint256);
|
|
125
125
|
|
|
126
|
+
/// @notice The minimum attestation grace period enforced during game initialization.
|
|
127
|
+
/// @return The minimum grace period in seconds.
|
|
128
|
+
function MIN_ATTESTATION_GRACE_PERIOD() external view returns (uint256);
|
|
129
|
+
|
|
126
130
|
/// @notice The quorum required to ratify a scorecard.
|
|
127
131
|
/// @param gameId The ID of the game.
|
|
128
132
|
/// @return The quorum threshold.
|
|
@@ -70,6 +70,11 @@ interface IDefifaHook is IJB721Hook {
|
|
|
70
70
|
/// @param caller The address that set the tier weights.
|
|
71
71
|
event TierCashOutWeightsSet(DefifaTierCashOutWeight[] tierWeights, address caller);
|
|
72
72
|
|
|
73
|
+
/// @notice Returns the adjusted pending reserve count for a tier, subtracting refund-phase burns.
|
|
74
|
+
/// @param tierId The tier ID.
|
|
75
|
+
/// @return The adjusted pending reserve count.
|
|
76
|
+
function adjustedPendingReservesFor(uint256 tierId) external view returns (uint256);
|
|
77
|
+
|
|
73
78
|
/// @notice The total amount redeemed from this game (refunds not counted).
|
|
74
79
|
/// @return The total redeemed amount.
|
|
75
80
|
function amountRedeemed() external view returns (uint256);
|
|
@@ -8,6 +8,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
|
|
|
8
8
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
9
9
|
|
|
10
10
|
import {DefifaGamePhase} from "../enums/DefifaGamePhase.sol";
|
|
11
|
+
import {IDefifaHook} from "../interfaces/IDefifaHook.sol";
|
|
11
12
|
import {DefifaTierCashOutWeight} from "../structs/DefifaTierCashOutWeight.sol";
|
|
12
13
|
|
|
13
14
|
/// @notice Pure/view helper functions extracted from DefifaHook to reduce contract bytecode size.
|
|
@@ -65,6 +66,10 @@ library DefifaHookLib {
|
|
|
65
66
|
// slither-disable-next-line calls-loop
|
|
66
67
|
tier = hookStore.tierOf({hook: hook, id: tierWeights[i].id, includeResolvedUri: false});
|
|
67
68
|
|
|
69
|
+
// Guard against uint32 truncation: if the caller passes a tier ID > type(uint32).max,
|
|
70
|
+
// the store may silently truncate and return a different tier.
|
|
71
|
+
if (tierWeights[i].id != tier.id) revert DefifaHook_InvalidTierId();
|
|
72
|
+
|
|
68
73
|
// Can't set a cashOut weight for tiers not in category 0.
|
|
69
74
|
if (tier.category != 0) revert DefifaHook_InvalidTierId();
|
|
70
75
|
|
|
@@ -129,13 +134,9 @@ library DefifaHookLib {
|
|
|
129
134
|
uint256 totalTokensForCashoutInTier =
|
|
130
135
|
tier.initialSupply - tier.remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId]);
|
|
131
136
|
|
|
132
|
-
// Include pending (unminted) reserve NFTs in the denominator
|
|
133
|
-
// could cash out before reserves are minted and extract value that should be diluted across
|
|
134
|
-
// both paid and reserved holders. By counting pending reserves, each token's share of the
|
|
135
|
-
// tier weight is computed against the full eventual supply.
|
|
137
|
+
// Include pending (unminted) reserve NFTs in the denominator, adjusted for refund-phase burns.
|
|
136
138
|
// slither-disable-next-line calls-loop
|
|
137
|
-
|
|
138
|
-
totalTokensForCashoutInTier += pendingReserves;
|
|
139
|
+
totalTokensForCashoutInTier += IDefifaHook(hook).adjustedPendingReservesFor(tierId);
|
|
139
140
|
|
|
140
141
|
// Calculate the percentage of the tier cashOut amount a single token counts for.
|
|
141
142
|
// Integer division rounding in cashOutWeight is unavoidable in Solidity. Rounding direction
|
|
@@ -210,8 +211,7 @@ library DefifaHookLib {
|
|
|
210
211
|
// slither-disable-next-line calls-loop
|
|
211
212
|
cumulativeMintPrice += hookStore.tierOfTokenId({
|
|
212
213
|
hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
|
|
213
|
-
})
|
|
214
|
-
.price;
|
|
214
|
+
}).price;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
// Calculate the user's claimable amount proportional to what they paid.
|
|
@@ -238,8 +238,7 @@ library DefifaHookLib {
|
|
|
238
238
|
// slither-disable-next-line calls-loop
|
|
239
239
|
cumulativeMintPrice += hookStore.tierOfTokenId({
|
|
240
240
|
hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
|
|
241
|
-
})
|
|
242
|
-
.price;
|
|
241
|
+
}).price;
|
|
243
242
|
}
|
|
244
243
|
}
|
|
245
244
|
|