@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.
- 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 +79 -47
- package/src/DefifaGovernor.sol +57 -12
- package/src/DefifaHook.sol +83 -26
- package/src/DefifaProjectOwner.sol +4 -3
- package/src/DefifaTokenUriResolver.sol +113 -20
- package/src/enums/DefifaGamePhase.sol +6 -0
- package/src/enums/DefifaScorecardState.sol +4 -0
- package/src/interfaces/IDefifaDeployer.sol +5 -0
- package/src/interfaces/IDefifaGamePhaseReporter.sol +4 -0
- package/src/interfaces/IDefifaGamePotReporter.sol +10 -0
- package/src/interfaces/IDefifaGovernor.sol +4 -0
- package/src/interfaces/IDefifaHook.sol +5 -0
- package/src/interfaces/IDefifaTokenUriResolver.sol +3 -0
- package/src/libraries/DefifaFontImporter.sol +1 -1
- package/src/libraries/DefifaHookLib.sol +9 -10
- package/src/structs/DefifaAttestations.sol +3 -2
- package/src/structs/DefifaDelegation.sol +1 -0
- package/src/structs/DefifaLaunchProjectData.sol +2 -3
- package/src/structs/DefifaOpsData.sol +1 -0
- package/src/structs/DefifaScorecard.sol +2 -0
- package/src/structs/DefifaTierCashOutWeight.sol +3 -1
- package/src/structs/DefifaTierParams.sol +1 -0
- 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
|
@@ -20,7 +20,10 @@ import {DefifaScorecard} from "./structs/DefifaScorecard.sol";
|
|
|
20
20
|
import {DefifaTierCashOutWeight} from "./structs/DefifaTierCashOutWeight.sol";
|
|
21
21
|
import {DefifaHookLib} from "./libraries/DefifaHookLib.sol";
|
|
22
22
|
|
|
23
|
-
/// @notice Manages the ratification of Defifa scorecards.
|
|
23
|
+
/// @notice Manages the ratification of Defifa scorecards through token-weighted attestation. After a game ends,
|
|
24
|
+
/// anyone can submit a scorecard proposing cash-out weights for each tier. NFT holders attest to scorecards using
|
|
25
|
+
/// their voting power (proportional to NFTs held). A scorecard is ratified once it reaches quorum and survives the
|
|
26
|
+
/// grace period, after which the deployer applies the weights to the game's treasury.
|
|
24
27
|
contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
25
28
|
//*********************************************************************//
|
|
26
29
|
// --------------------------- custom errors ------------------------- //
|
|
@@ -31,6 +34,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
31
34
|
error DefifaGovernor_AlreadyRatified();
|
|
32
35
|
error DefifaGovernor_DuplicateScorecard();
|
|
33
36
|
error DefifaGovernor_GameNotFound();
|
|
37
|
+
error DefifaGovernor_GracePeriodTooShort();
|
|
34
38
|
error DefifaGovernor_IncorrectTierOrder();
|
|
35
39
|
error DefifaGovernor_NotAllowed();
|
|
36
40
|
error DefifaGovernor_NotAttested();
|
|
@@ -45,6 +49,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
45
49
|
/// @notice The max attestation power each tier has if every token within the tier attestations.
|
|
46
50
|
uint256 public constant override MAX_ATTESTATION_POWER_TIER = 1_000_000_000;
|
|
47
51
|
|
|
52
|
+
/// @notice The minimum attestation grace period enforced during game initialization.
|
|
53
|
+
uint256 public constant override MIN_ATTESTATION_GRACE_PERIOD = 1 days;
|
|
54
|
+
|
|
48
55
|
//*********************************************************************//
|
|
49
56
|
// --------------- public immutable stored properties ---------------- //
|
|
50
57
|
//*********************************************************************//
|
|
@@ -104,6 +111,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
104
111
|
/// @custom:param tierId The tier ID (0-indexed).
|
|
105
112
|
mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) internal _scorecardTierWeightsOf;
|
|
106
113
|
|
|
114
|
+
/// @notice The timestamp when quorum was first reached for a scorecard.
|
|
115
|
+
/// @dev Reset to 0 if attestations drop below quorum via revocation.
|
|
116
|
+
/// @custom:param gameId The ID of the game.
|
|
117
|
+
/// @custom:param scorecardId The ID of the scorecard.
|
|
118
|
+
mapping(uint256 => mapping(uint256 => uint48)) internal _quorumReachedAtOf;
|
|
119
|
+
|
|
107
120
|
//*********************************************************************//
|
|
108
121
|
// -------------------------- constructor ---------------------------- //
|
|
109
122
|
//*********************************************************************//
|
|
@@ -164,6 +177,11 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
164
177
|
// Increase the attestation count.
|
|
165
178
|
attestations.count += weight;
|
|
166
179
|
|
|
180
|
+
// Record when quorum is first reached so the timelock anchors to this moment.
|
|
181
|
+
if (_quorumReachedAtOf[gameId][scorecardId] == 0 && attestations.count >= scorecard.quorumSnapshot) {
|
|
182
|
+
_quorumReachedAtOf[gameId][scorecardId] = uint48(block.timestamp);
|
|
183
|
+
}
|
|
184
|
+
|
|
167
185
|
// Store the BWA weight that was added (used for accurate subtraction on revoke).
|
|
168
186
|
attestations.attestedWeightOf[msg.sender] = weight;
|
|
169
187
|
|
|
@@ -237,6 +255,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
237
255
|
attestations.count -= weight;
|
|
238
256
|
attestations.attestedWeightOf[msg.sender] = 0;
|
|
239
257
|
|
|
258
|
+
// Reset quorum timestamp if attestations drop below quorum.
|
|
259
|
+
DefifaScorecard storage scorecard = _scorecardOf[gameId][scorecardId];
|
|
260
|
+
if (attestations.count < scorecard.quorumSnapshot) {
|
|
261
|
+
_quorumReachedAtOf[gameId][scorecardId] = 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
240
264
|
emit AttestationRevoked(gameId, scorecardId, msg.sender, weight);
|
|
241
265
|
}
|
|
242
266
|
|
|
@@ -302,17 +326,24 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
302
326
|
if (scorecard.attestationsBegin != 0) revert DefifaGovernor_DuplicateScorecard();
|
|
303
327
|
|
|
304
328
|
uint256 attestationStartTime = attestationStartTimeOf(gameId);
|
|
329
|
+
|
|
330
|
+
// Game phase timing is timestamp-based by design.
|
|
331
|
+
uint256 currentTimestamp = block.timestamp;
|
|
305
332
|
uint256 timeUntilAttestationsBegin =
|
|
306
|
-
|
|
333
|
+
currentTimestamp > attestationStartTime ? 0 : attestationStartTime - currentTimestamp;
|
|
307
334
|
|
|
308
335
|
// Casting to uint48 is safe because block.timestamp fits in uint48 until year 8921556.
|
|
309
336
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
310
|
-
uint48 attestationsBegin = uint48(
|
|
337
|
+
uint48 attestationsBegin = uint48(currentTimestamp + timeUntilAttestationsBegin);
|
|
311
338
|
scorecard.attestationsBegin = attestationsBegin;
|
|
312
339
|
// Grace period extends from when attestations begin, not from submission time.
|
|
313
340
|
// This prevents the grace period from expiring before attestations even start
|
|
314
341
|
// when a scorecard is submitted early.
|
|
315
|
-
|
|
342
|
+
uint256 gracePeriodEnds = uint256(attestationsBegin) + attestationGracePeriodOf(gameId);
|
|
343
|
+
if (gracePeriodEnds > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
344
|
+
// Safe after the explicit max check above.
|
|
345
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
346
|
+
scorecard.gracePeriodEnds = uint48(gracePeriodEnds);
|
|
316
347
|
|
|
317
348
|
// Store tier weights for BWA computation.
|
|
318
349
|
for (uint256 i; i < numberOfTierWeights;) {
|
|
@@ -335,9 +366,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
335
366
|
// slither-disable-next-line calls-loop
|
|
336
367
|
JB721Tier memory tier =
|
|
337
368
|
hookStore.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
|
|
369
|
+
// Use adjusted pending reserves that account for refund-phase burns.
|
|
338
370
|
// slither-disable-next-line calls-loop
|
|
339
|
-
uint256 pendingReserves =
|
|
340
|
-
hookStore.numberOfPendingReservesFor({hook: metadata.dataHook, tierId: tierId});
|
|
371
|
+
uint256 pendingReserves = IDefifaHook(metadata.dataHook).adjustedPendingReservesFor(tierId);
|
|
341
372
|
// slither-disable-next-line calls-loop
|
|
342
373
|
uint256 submittedTierAttestationUnits =
|
|
343
374
|
IDefifaHook(metadata.dataHook).currentSupplyOfTier(tierId) * tier.votingUnits;
|
|
@@ -457,8 +488,8 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
457
488
|
// Set a default attestation start time if needed.
|
|
458
489
|
if (attestationStartTime == 0) attestationStartTime = block.timestamp;
|
|
459
490
|
|
|
460
|
-
// Enforce a minimum grace period
|
|
461
|
-
if (attestationGracePeriod <
|
|
491
|
+
// Enforce a minimum grace period to prevent instant ratification.
|
|
492
|
+
if (attestationGracePeriod < MIN_ATTESTATION_GRACE_PERIOD) revert DefifaGovernor_GracePeriodTooShort();
|
|
462
493
|
|
|
463
494
|
// Ensure values fit within their allocated 48-bit widths before packing.
|
|
464
495
|
if (attestationStartTime > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
@@ -554,8 +585,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
554
585
|
// When the reserve beneficiary later mints, their new NFTs add to the numerator while
|
|
555
586
|
// pending reserves decrease by the same amount — so no one's voting power shifts.
|
|
556
587
|
{
|
|
588
|
+
// Use adjusted pending reserves that account for refund-phase burns.
|
|
557
589
|
// slither-disable-next-line calls-loop
|
|
558
|
-
uint256 pendingReserves =
|
|
590
|
+
uint256 pendingReserves = IDefifaHook(metadata.dataHook).adjustedPendingReservesFor(tierId);
|
|
559
591
|
if (pendingReserves != 0) {
|
|
560
592
|
// slither-disable-next-line calls-loop
|
|
561
593
|
JB721Tier memory tier =
|
|
@@ -700,8 +732,9 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
700
732
|
// burner erase another participant's quorum contribution without erasing their claim.
|
|
701
733
|
// slither-disable-next-line calls-loop
|
|
702
734
|
uint256 currentTierSupply = hook.currentSupplyOfTier(tierId);
|
|
735
|
+
// Use adjusted pending reserves that account for refund-phase burns.
|
|
703
736
|
// slither-disable-next-line calls-loop
|
|
704
|
-
uint256 pendingReserves =
|
|
737
|
+
uint256 pendingReserves = hook.adjustedPendingReservesFor(tierId);
|
|
705
738
|
if (currentTierSupply != 0 || pendingReserves != 0) {
|
|
706
739
|
eligibleTierWeights += MAX_ATTESTATION_POWER_TIER;
|
|
707
740
|
}
|
|
@@ -744,12 +777,16 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
744
777
|
|
|
745
778
|
// If the scorecard has attestations beginning in the future, the state is PENDING.
|
|
746
779
|
// At exactly `attestationsBegin`, attestations are open so the state is ACTIVE.
|
|
780
|
+
// Game phase timing is timestamp-based by design.
|
|
781
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
747
782
|
if (scorecard.attestationsBegin > block.timestamp) {
|
|
748
783
|
return DefifaScorecardState.PENDING;
|
|
749
784
|
}
|
|
750
785
|
|
|
751
786
|
// If the scorecard's grace period has not yet ended, the state is ACTIVE.
|
|
752
787
|
// At exactly `gracePeriodEnds`, the grace period has elapsed so we fall through to the quorum check.
|
|
788
|
+
// Game phase timing is timestamp-based by design.
|
|
789
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
753
790
|
if (scorecard.gracePeriodEnds > block.timestamp) {
|
|
754
791
|
return DefifaScorecardState.ACTIVE;
|
|
755
792
|
}
|
|
@@ -757,8 +794,16 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
757
794
|
// If quorum has been reached (using the concentration-adjusted snapshot), check timelock.
|
|
758
795
|
if (scorecard.quorumSnapshot <= _scorecardAttestationsOf[gameId][scorecardId].count) {
|
|
759
796
|
uint256 timelockDur = timelockDurationOf(gameId);
|
|
760
|
-
if (timelockDur > 0
|
|
761
|
-
|
|
797
|
+
if (timelockDur > 0) {
|
|
798
|
+
// Anchor the timelock to the later of grace period end or when quorum was reached.
|
|
799
|
+
uint256 quorumReachedAt = _quorumReachedAtOf[gameId][scorecardId];
|
|
800
|
+
uint256 timelockAnchor =
|
|
801
|
+
quorumReachedAt > scorecard.gracePeriodEnds ? quorumReachedAt : uint256(scorecard.gracePeriodEnds);
|
|
802
|
+
// Game phase timing is timestamp-based by design.
|
|
803
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
804
|
+
if (block.timestamp < timelockAnchor + timelockDur) {
|
|
805
|
+
return DefifaScorecardState.QUEUED;
|
|
806
|
+
}
|
|
762
807
|
}
|
|
763
808
|
return DefifaScorecardState.SUCCEEDED;
|
|
764
809
|
}
|
package/src/DefifaHook.sol
CHANGED
|
@@ -36,7 +36,10 @@ import {DefifaTierCashOutWeight} from "./structs/DefifaTierCashOutWeight.sol";
|
|
|
36
36
|
import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
|
|
37
37
|
import {DefifaHookLib} from "./libraries/DefifaHookLib.sol";
|
|
38
38
|
|
|
39
|
-
/// @notice
|
|
39
|
+
/// @notice The 721 hook that powers Defifa games. Extends JB721Hook to enforce game phase rules on minting and
|
|
40
|
+
/// cashing out, track per-tier voting power via checkpoints, and apply scorecard-determined cash-out weights after
|
|
41
|
+
/// ratification. Mints are only allowed during the MINT phase, refunds during the REFUND phase, and cash outs
|
|
42
|
+
/// with scoring weights only after a scorecard is ratified in the COMPLETE phase.
|
|
40
43
|
contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
41
44
|
using Checkpoints for Checkpoints.Trace208;
|
|
42
45
|
|
|
@@ -45,6 +48,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
45
48
|
//*********************************************************************//
|
|
46
49
|
|
|
47
50
|
error DefifaHook_BadTierOrder();
|
|
51
|
+
error DefifaHook_IdenticalTokens();
|
|
48
52
|
error DefifaHook_DelegateAddressZero();
|
|
49
53
|
error DefifaHook_DelegateChangesUnavailableInThisPhase();
|
|
50
54
|
error DefifaHook_GameIsntScoringYet();
|
|
@@ -116,14 +120,23 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
116
120
|
// --------------------- public stored properties -------------------- //
|
|
117
121
|
//*********************************************************************//
|
|
118
122
|
|
|
123
|
+
/// @notice The amount that has been redeemed from this game, refunds are not counted.
|
|
124
|
+
uint256 public override amountRedeemed;
|
|
125
|
+
|
|
126
|
+
/// @notice The common base for the tokenUri's
|
|
127
|
+
string public override baseURI;
|
|
128
|
+
|
|
129
|
+
/// @notice A flag indicating if the cashout weights has been set.
|
|
130
|
+
bool public override cashOutWeightIsSet;
|
|
131
|
+
|
|
119
132
|
/// @notice The address of the origin 'DefifaHook', used to check in the init if the contract is the original or not
|
|
120
133
|
address public immutable override CODE_ORIGIN;
|
|
121
134
|
|
|
122
|
-
/// @notice
|
|
123
|
-
|
|
135
|
+
/// @notice Contract metadata uri.
|
|
136
|
+
string public override contractURI;
|
|
124
137
|
|
|
125
|
-
/// @notice The
|
|
126
|
-
|
|
138
|
+
/// @notice The address that'll be set as the attestation delegate by default.
|
|
139
|
+
address public override defaultAttestationDelegate;
|
|
127
140
|
|
|
128
141
|
/// @notice The contract reporting game phases.
|
|
129
142
|
IDefifaGamePhaseReporter public override gamePhaseReporter;
|
|
@@ -138,20 +151,16 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
138
151
|
/// @notice The currency that is accepted when minting tier NFTs.
|
|
139
152
|
uint256 public override pricingCurrency;
|
|
140
153
|
|
|
141
|
-
/// @notice
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
string public override baseURI;
|
|
146
|
-
|
|
147
|
-
/// @notice Contract metadata uri.
|
|
148
|
-
string public override contractURI;
|
|
154
|
+
/// @notice The number of tokens burned from a tier during non-COMPLETE phases (refund, no-contest).
|
|
155
|
+
/// @dev Used to adjust pending reserve counts so reserves that correspond to refunded mints
|
|
156
|
+
/// are excluded from the cash-out denominator.
|
|
157
|
+
mapping(uint256 => uint256) public refundedBurnsFrom;
|
|
149
158
|
|
|
150
|
-
/// @notice The
|
|
151
|
-
|
|
159
|
+
/// @notice The contract storing all funding cycle configurations.
|
|
160
|
+
IJBRulesets public override rulesets;
|
|
152
161
|
|
|
153
|
-
/// @notice The
|
|
154
|
-
|
|
162
|
+
/// @notice The contract that stores and manages the NFT's data.
|
|
163
|
+
IJB721TiersHookStore public override store;
|
|
155
164
|
|
|
156
165
|
/// @notice The amount of tokens that have been redeemed from a tier, refunds are not counted.
|
|
157
166
|
/// @custom:param The tier from which tokens have been redeemed.
|
|
@@ -161,6 +170,41 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
161
170
|
// ------------------------- external views -------------------------- //
|
|
162
171
|
//*********************************************************************//
|
|
163
172
|
|
|
173
|
+
/// @notice Returns the adjusted pending reserve count for a tier, accounting for refund-phase burns.
|
|
174
|
+
/// @dev Recalculates reserves from (paidMints - burns) / reserveFrequency since the relationship
|
|
175
|
+
/// between burns and reserves is not linear — it depends on the tier's reserve frequency.
|
|
176
|
+
/// @param tierId The tier ID.
|
|
177
|
+
/// @return The adjusted pending reserve count (floored at 0).
|
|
178
|
+
// slither-disable-next-line calls-loop
|
|
179
|
+
function adjustedPendingReservesFor(uint256 tierId) public view returns (uint256) {
|
|
180
|
+
uint256 refundBurns = refundedBurnsFrom[tierId];
|
|
181
|
+
|
|
182
|
+
// If no refund burns, return the store's value directly.
|
|
183
|
+
if (refundBurns == 0) return store.numberOfPendingReservesFor({hook: address(this), tierId: tierId});
|
|
184
|
+
|
|
185
|
+
// Get the tier to access reserveFrequency and supply data.
|
|
186
|
+
JB721Tier memory tier = store.tierOf({hook: address(this), id: tierId, includeResolvedUri: false});
|
|
187
|
+
|
|
188
|
+
// No reserves if no reserve frequency.
|
|
189
|
+
if (tier.reserveFrequency == 0) return 0;
|
|
190
|
+
|
|
191
|
+
// Calculate the number of reserves already minted.
|
|
192
|
+
uint256 reservesMinted = store.numberOfReservesMintedFor({hook: address(this), tierId: tierId});
|
|
193
|
+
|
|
194
|
+
// Calculate non-reserve mints: initialSupply - remainingSupply - reservesMinted.
|
|
195
|
+
uint256 nonReserveMints = tier.initialSupply - tier.remainingSupply - reservesMinted;
|
|
196
|
+
|
|
197
|
+
// Subtract refund burns from non-reserve mints (burns can't exceed non-reserve mints).
|
|
198
|
+
uint256 adjustedMints = nonReserveMints > refundBurns ? nonReserveMints - refundBurns : 0;
|
|
199
|
+
|
|
200
|
+
// Recalculate available reserves: ceil(adjustedMints / reserveFrequency).
|
|
201
|
+
uint256 availableReserves = adjustedMints / tier.reserveFrequency;
|
|
202
|
+
if (adjustedMints % tier.reserveFrequency > 0) ++availableReserves;
|
|
203
|
+
|
|
204
|
+
// Return pending = available - already minted (floored at 0).
|
|
205
|
+
return availableReserves > reservesMinted ? availableReserves - reservesMinted : 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
/// @notice The first owner of each token ID, which corresponds to the address that originally contributed to the
|
|
165
209
|
/// project to receive the NFT.
|
|
166
210
|
/// @param tokenId The ID of the token to get the first owner of.
|
|
@@ -304,8 +348,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
304
348
|
// slither-disable-next-line calls-loop
|
|
305
349
|
cumulativeMintPrice -= hookStore.tierOfTokenId({
|
|
306
350
|
hook: address(this), tokenId: decodedTokenIds[i], includeResolvedUri: false
|
|
307
|
-
})
|
|
308
|
-
.price;
|
|
351
|
+
}).price;
|
|
309
352
|
}
|
|
310
353
|
|
|
311
354
|
unchecked {
|
|
@@ -418,12 +461,15 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
418
461
|
// If the game isn't complete, we do not have any tokens to claim.
|
|
419
462
|
if (gamePhaseReporter.currentGamePhaseOf(PROJECT_ID) != DefifaGamePhase.COMPLETE) return (0, 0);
|
|
420
463
|
|
|
464
|
+
// Include unminted reserves in the denominator. Once reserves are pending, their future recipients are
|
|
465
|
+
// entitled to fee-token claims as if the reserve NFTs had already been minted; otherwise paid holders could
|
|
466
|
+
// claim too large a share before the reserve mint transaction lands.
|
|
421
467
|
// slither-disable-next-line unused-return
|
|
422
468
|
return DefifaHookLib.computeTokensClaim({
|
|
423
469
|
tokenIds: tokenIds,
|
|
424
470
|
hookStore: store,
|
|
425
471
|
hook: address(this),
|
|
426
|
-
totalMintCost: _totalMintCost,
|
|
472
|
+
totalMintCost: _totalMintCost + _pendingReserveMintCost(),
|
|
427
473
|
defifaBalance: DEFIFA_TOKEN.balanceOf(address(this)),
|
|
428
474
|
baseProtocolBalance: BASE_PROTOCOL_TOKEN.balanceOf(address(this))
|
|
429
475
|
});
|
|
@@ -449,6 +495,8 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
449
495
|
JB721Hook(_directory)
|
|
450
496
|
Ownable(msg.sender)
|
|
451
497
|
{
|
|
498
|
+
if (address(_defifaToken) == address(_baseProtocolToken)) revert DefifaHook_IdenticalTokens();
|
|
499
|
+
|
|
452
500
|
CODE_ORIGIN = address(this);
|
|
453
501
|
DEFIFA_TOKEN = _defifaToken;
|
|
454
502
|
BASE_PROTOCOL_TOKEN = _baseProtocolToken;
|
|
@@ -574,6 +622,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
574
622
|
/// @notice Mint reserved tokens within the tier for the provided value.
|
|
575
623
|
/// @param tierId The ID of the tier to mint within.
|
|
576
624
|
/// @param count The number of reserved tokens to mint.
|
|
625
|
+
// slither-disable-next-line reentrancy-benign,reentrancy-no-eth
|
|
577
626
|
function mintReservesFor(uint256 tierId, uint256 count) public override {
|
|
578
627
|
// Minting reserves must not be paused.
|
|
579
628
|
// slither-disable-next-line calls-loop
|
|
@@ -703,13 +752,20 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
703
752
|
}
|
|
704
753
|
|
|
705
754
|
// Burn the token.
|
|
755
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
706
756
|
_burn(tokenId);
|
|
707
757
|
|
|
708
|
-
//
|
|
758
|
+
// slither-disable-next-line calls-loop
|
|
759
|
+
uint256 tierId = hookStore.tierIdOfToken(tokenId);
|
|
709
760
|
if (isComplete) {
|
|
761
|
+
// Track per-tier redemptions during the complete phase.
|
|
762
|
+
unchecked {
|
|
763
|
+
++tokensRedeemedFrom[tierId];
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
// Track non-COMPLETE burns (refund/no-contest) so pending reserve counts can be adjusted.
|
|
710
767
|
unchecked {
|
|
711
|
-
|
|
712
|
-
++tokensRedeemedFrom[hookStore.tierIdOfToken(tokenId)];
|
|
768
|
+
++refundedBurnsFrom[tierId];
|
|
713
769
|
}
|
|
714
770
|
}
|
|
715
771
|
|
|
@@ -735,7 +791,7 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
735
791
|
// are accounted for, preventing paid holders from claiming a disproportionate share.
|
|
736
792
|
// slither-disable-next-line reentrancy-events
|
|
737
793
|
beneficiaryReceivedTokens = _claimTokensFor({
|
|
738
|
-
beneficiary: context.
|
|
794
|
+
beneficiary: context.beneficiary,
|
|
739
795
|
shareToBeneficiary: cumulativeMintPrice,
|
|
740
796
|
outOfTotal: _totalMintCost + _pendingReserveMintCost()
|
|
741
797
|
});
|
|
@@ -854,11 +910,12 @@ contract DefifaHook is JB721Hook, Ownable, IDefifaHook {
|
|
|
854
910
|
|
|
855
911
|
for (uint256 i; i < numberOfTiers;) {
|
|
856
912
|
uint256 tierId = i + 1;
|
|
857
|
-
|
|
858
|
-
uint256 pendingReserves = hookStore.numberOfPendingReservesFor({hook: address(this), tierId: tierId});
|
|
913
|
+
uint256 pendingReserves = adjustedPendingReservesFor(tierId);
|
|
859
914
|
if (pendingReserves != 0) {
|
|
860
915
|
// slither-disable-next-line calls-loop
|
|
861
916
|
JB721Tier memory tier = hookStore.tierOf({hook: address(this), id: tierId, includeResolvedUri: false});
|
|
917
|
+
|
|
918
|
+
// Pending reserves dilute claims by the same economic weight as paid mints at this tier's price.
|
|
862
919
|
cost += pendingReserves * tier.price;
|
|
863
920
|
}
|
|
864
921
|
unchecked {
|
|
@@ -8,9 +8,10 @@ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Recei
|
|
|
8
8
|
|
|
9
9
|
import {DefifaDeployer} from "./DefifaDeployer.sol";
|
|
10
10
|
|
|
11
|
-
/// @notice A
|
|
12
|
-
///
|
|
13
|
-
///
|
|
11
|
+
/// @notice A dead-end owner for Defifa project NFTs. When the project NFT is transferred here, this contract
|
|
12
|
+
/// permanently holds it and grants SET_SPLIT_GROUPS permission to the Defifa deployer — allowing the deployer to
|
|
13
|
+
/// manage splits without any human having project ownership.
|
|
14
|
+
/// @dev Once the project NFT is transferred here, it cannot be recovered.
|
|
14
15
|
contract DefifaProjectOwner is IERC721Receiver {
|
|
15
16
|
//*********************************************************************//
|
|
16
17
|
// --------------------------- custom errors ------------------------- //
|
|
@@ -19,7 +19,8 @@ import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
|
|
|
19
19
|
import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
|
|
20
20
|
import {IDefifaTokenUriResolver} from "./interfaces/IDefifaTokenUriResolver.sol";
|
|
21
21
|
|
|
22
|
-
/// @notice
|
|
22
|
+
/// @notice Generates on-chain SVG token URIs for Defifa game NFTs. Each NFT image shows the tier name, game phase,
|
|
23
|
+
/// and current cash-out value. Uses an on-chain typeface for rendering text within the SVG.
|
|
23
24
|
contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolver {
|
|
24
25
|
using Strings for uint256;
|
|
25
26
|
|
|
@@ -70,12 +71,13 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
70
71
|
// Keep a reference to the rarity text;
|
|
71
72
|
string memory valueText;
|
|
72
73
|
|
|
73
|
-
// Keep a reference to the game's name.
|
|
74
|
+
// Keep a reference to the game's name (escaped for JSON/SVG safety).
|
|
74
75
|
// TODO: Somehow make the `IDefifaHook` have the `name` function.
|
|
75
|
-
string memory
|
|
76
|
+
string memory titleSvg = _escapeSvg(ERC721(address(hook)).name());
|
|
76
77
|
|
|
77
78
|
// Keep a reference to the tier's name.
|
|
78
|
-
string memory
|
|
79
|
+
string memory teamJson;
|
|
80
|
+
string memory teamSvg;
|
|
79
81
|
|
|
80
82
|
// Keep a reference to the SVG parts.
|
|
81
83
|
string[] memory parts = new string[](4);
|
|
@@ -88,8 +90,10 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
88
90
|
JB721Tier memory tier =
|
|
89
91
|
hook.store().tierOfTokenId({hook: address(hook), tokenId: tokenId, includeResolvedUri: false});
|
|
90
92
|
|
|
91
|
-
// Set the tier's name.
|
|
92
|
-
|
|
93
|
+
// Set the tier's name (escaped for JSON/SVG safety).
|
|
94
|
+
string memory rawTeam = hook.tierNameOf(tier.id);
|
|
95
|
+
teamJson = _escapeJson(rawTeam);
|
|
96
|
+
teamSvg = _escapeSvg(rawTeam);
|
|
93
97
|
|
|
94
98
|
// Check to see if the tier has a URI. Return it if it does.
|
|
95
99
|
if (tier.encodedIPFSUri != bytes32(0)) {
|
|
@@ -101,11 +105,11 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
101
105
|
parts[1] = string(
|
|
102
106
|
abi.encodePacked(
|
|
103
107
|
'{"name":"',
|
|
104
|
-
|
|
108
|
+
teamJson,
|
|
105
109
|
'", "id": "',
|
|
106
110
|
uint256(tier.id).toString(),
|
|
107
111
|
'","description":"Team: ',
|
|
108
|
-
|
|
112
|
+
teamJson,
|
|
109
113
|
", ID: ",
|
|
110
114
|
uint256(tier.id).toString(),
|
|
111
115
|
'.","image":"data:image/svg+xml;base64,'
|
|
@@ -201,27 +205,27 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
201
205
|
gamePhaseText,
|
|
202
206
|
"</text>",
|
|
203
207
|
'<text x="10" y="85" style="font-size:26px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">',
|
|
204
|
-
_getSubstring(
|
|
208
|
+
_getSubstring(titleSvg, 0, 30),
|
|
205
209
|
"</text>",
|
|
206
210
|
'<text x="10" y="120" style="font-size:26px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">',
|
|
207
|
-
_getSubstring(
|
|
211
|
+
_getSubstring(titleSvg, 30, 60),
|
|
208
212
|
"</text>",
|
|
209
213
|
'<text x="10" y="205" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
|
|
210
|
-
bytes(_getSubstring(
|
|
211
|
-
? _getSubstring(
|
|
214
|
+
bytes(_getSubstring(teamSvg, 20, 30)).length != 0 && bytes(_getSubstring(teamSvg, 10, 20)).length != 0
|
|
215
|
+
? _getSubstring(teamSvg, 0, 10)
|
|
212
216
|
: "",
|
|
213
217
|
"</text>",
|
|
214
218
|
'<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(
|
|
219
|
+
bytes(_getSubstring(teamSvg, 20, 30)).length != 0
|
|
220
|
+
? _getSubstring(teamSvg, 10, 20)
|
|
221
|
+
: bytes(_getSubstring(teamSvg, 10, 20)).length != 0 ? _getSubstring(teamSvg, 0, 10) : "",
|
|
218
222
|
"</text>",
|
|
219
223
|
'<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(
|
|
224
|
+
bytes(_getSubstring(teamSvg, 20, 30)).length != 0
|
|
225
|
+
? _getSubstring(teamSvg, 20, 30)
|
|
226
|
+
: bytes(_getSubstring(teamSvg, 10, 20)).length != 0
|
|
227
|
+
? _getSubstring(teamSvg, 10, 20)
|
|
228
|
+
: _getSubstring(teamSvg, 0, 10),
|
|
225
229
|
"</text>",
|
|
226
230
|
'<text x="10" y="430" style="font-size:16px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">TOKEN ID: ',
|
|
227
231
|
tokenId.toString(),
|
|
@@ -312,4 +316,93 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
|
|
|
312
316
|
}
|
|
313
317
|
return string(result);
|
|
314
318
|
}
|
|
319
|
+
|
|
320
|
+
/// @notice Escapes special characters for safe JSON string interpolation.
|
|
321
|
+
/// @param input The raw string.
|
|
322
|
+
/// @return The escaped string.
|
|
323
|
+
function _escapeJson(string memory input) internal pure returns (string memory) {
|
|
324
|
+
bytes memory b = bytes(input);
|
|
325
|
+
// Worst case: every char needs escaping (2x length).
|
|
326
|
+
bytes memory out = new bytes(b.length * 2);
|
|
327
|
+
uint256 j;
|
|
328
|
+
for (uint256 i; i < b.length;) {
|
|
329
|
+
bytes1 c = b[i];
|
|
330
|
+
if (c == '"' || c == "\\") {
|
|
331
|
+
out[j++] = "\\";
|
|
332
|
+
out[j++] = c;
|
|
333
|
+
} else if (c == 0x0a) {
|
|
334
|
+
// newline
|
|
335
|
+
out[j++] = "\\";
|
|
336
|
+
out[j++] = "n";
|
|
337
|
+
} else if (c == 0x0d) {
|
|
338
|
+
// carriage return
|
|
339
|
+
out[j++] = "\\";
|
|
340
|
+
out[j++] = "r";
|
|
341
|
+
} else {
|
|
342
|
+
out[j++] = c;
|
|
343
|
+
}
|
|
344
|
+
unchecked {
|
|
345
|
+
++i;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Trim to actual length.
|
|
349
|
+
bytes memory trimmed = new bytes(j);
|
|
350
|
+
for (uint256 k; k < j;) {
|
|
351
|
+
trimmed[k] = out[k];
|
|
352
|
+
unchecked {
|
|
353
|
+
++k;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return string(trimmed);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// @notice Escapes special characters for safe SVG text interpolation.
|
|
360
|
+
/// @param input The raw string.
|
|
361
|
+
/// @return The escaped string.
|
|
362
|
+
function _escapeSvg(string memory input) internal pure returns (string memory) {
|
|
363
|
+
bytes memory b = bytes(input);
|
|
364
|
+
// Worst case: each char becomes 5 chars (&).
|
|
365
|
+
bytes memory out = new bytes(b.length * 5);
|
|
366
|
+
uint256 j;
|
|
367
|
+
for (uint256 i; i < b.length;) {
|
|
368
|
+
bytes1 c = b[i];
|
|
369
|
+
if (c == "&") {
|
|
370
|
+
out[j++] = "&";
|
|
371
|
+
out[j++] = "a";
|
|
372
|
+
out[j++] = "m";
|
|
373
|
+
out[j++] = "p";
|
|
374
|
+
out[j++] = ";";
|
|
375
|
+
} else if (c == "<") {
|
|
376
|
+
out[j++] = "&";
|
|
377
|
+
out[j++] = "l";
|
|
378
|
+
out[j++] = "t";
|
|
379
|
+
out[j++] = ";";
|
|
380
|
+
} else if (c == ">") {
|
|
381
|
+
out[j++] = "&";
|
|
382
|
+
out[j++] = "g";
|
|
383
|
+
out[j++] = "t";
|
|
384
|
+
out[j++] = ";";
|
|
385
|
+
} else if (c == '"') {
|
|
386
|
+
out[j++] = "&";
|
|
387
|
+
out[j++] = "q";
|
|
388
|
+
out[j++] = "u";
|
|
389
|
+
out[j++] = "o";
|
|
390
|
+
out[j++] = "t";
|
|
391
|
+
out[j++] = ";";
|
|
392
|
+
} else {
|
|
393
|
+
out[j++] = c;
|
|
394
|
+
}
|
|
395
|
+
unchecked {
|
|
396
|
+
++i;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
bytes memory trimmed = new bytes(j);
|
|
400
|
+
for (uint256 k; k < j;) {
|
|
401
|
+
trimmed[k] = out[k];
|
|
402
|
+
unchecked {
|
|
403
|
+
++k;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return string(trimmed);
|
|
407
|
+
}
|
|
315
408
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
+
/// @notice The lifecycle phases of a Defifa game.
|
|
5
|
+
/// COUNTDOWN — before minting opens. MINT — players can mint tier NFTs. REFUND — minting closed but refunds
|
|
6
|
+
/// allowed.
|
|
7
|
+
/// SCORING — event has ended, scorecards can be submitted and attested. COMPLETE — scorecard ratified, cash outs
|
|
8
|
+
/// open.
|
|
9
|
+
/// NO_CONTEST — game voided (minimum participation not met or scorecard timed out), full refunds available.
|
|
4
10
|
enum DefifaGamePhase {
|
|
5
11
|
COUNTDOWN,
|
|
6
12
|
MINT,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
+
/// @notice The governance lifecycle of a submitted scorecard.
|
|
5
|
+
/// PENDING — submitted but attestation period hasn't started. ACTIVE — accepting attestations.
|
|
6
|
+
/// DEFEATED — failed to reach quorum. SUCCEEDED — reached quorum, in grace period.
|
|
7
|
+
/// QUEUED — grace period passed, awaiting application. RATIFIED — applied to the game's cash-out weights.
|
|
4
8
|
enum DefifaScorecardState {
|
|
5
9
|
PENDING,
|
|
6
10
|
ACTIVE,
|
|
@@ -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);
|
|
@@ -3,6 +3,10 @@ pragma solidity ^0.8.0;
|
|
|
3
3
|
|
|
4
4
|
import {DefifaGamePhase} from "../enums/DefifaGamePhase.sol";
|
|
5
5
|
|
|
6
|
+
/// @notice Reports the current lifecycle phase of a Defifa game.
|
|
6
7
|
interface IDefifaGamePhaseReporter {
|
|
8
|
+
/// @notice The current phase of a game (COUNTDOWN, MINT, REFUND, SCORING, COMPLETE, or NO_CONTEST).
|
|
9
|
+
/// @param gameId The ID of the game.
|
|
10
|
+
/// @return The current game phase.
|
|
7
11
|
function currentGamePhaseOf(uint256 gameId) external view returns (DefifaGamePhase);
|
|
8
12
|
}
|