Audit seed
Nemesis fresh round / defifa all src/**/*.sol and script/**/*.sol / lifecycle-state coupling, governance, settlement.
Repos involved
defifa
- Boundary context:
nana-core-v6, nana-721-hook-v6
Root cause
DefifaDeployer.launchGameWith only rejects tiers.length == 1 && scorecardTimeout == 0, but governance reachability is determined later by runtime participated tiers, not configured tier count. A multi-tier game with scorecardTimeout == 0 can enter scoring with only one tier having supply. At that point the only valid scorecard assigns 100% cash-out weight to that tier, which gives its holders zero BWA power, while quorum() still returns a nonzero threshold for the one participated tier.
Relevant code:
defifa/src/DefifaDeployer.sol:428-440 rejects only one configured tier with zero timeout.
defifa/src/DefifaDeployer.sol:261-267 only enters NO_CONTEST on timeout when scorecardTimeout > 0.
defifa/src/DefifaGovernor.sol:301-309 rejects nonzero scorecard weights for unowned tiers.
defifa/src/libraries/DefifaHookLib.sol:444-448 requires weights to sum to TOTAL_CASHOUT_WEIGHT.
defifa/src/DefifaGovernor.sol:703-707 sets BWA multiplier to zero for a 100%-weighted tier.
defifa/src/DefifaGovernor.sol:173-177 rejects zero-weight attestations.
defifa/src/DefifaGovernor.sol:739-759 returns a nonzero quorum for one participated tier.
defifa/src/DefifaDeployer.sol:646-650 prevents no-contest triggering unless the phase already reports NO_CONTEST.
defifa/src/DefifaHook.sol:744-751 reverts cash-out when no reclaim/fee-token claim exists.
Impact
Funds paid into a validly launched multi-tier game can be stuck with no on-chain final settlement or no-contest refund path if only one tier participates before scoring. This can happen naturally in one-sided markets, and a malicious launcher can increase the likelihood by disabling the timeout and configuring unattractive alternative tiers.
Severity: MEDIUM. The impact is a permanent liveness/settlement failure for affected game participants, but it depends on a zero-timeout configuration and one-sided participation.
Proof of concept
A temporary Foundry test was run locally:
forge test --match-path 'test/audit/CodexNemesisSingleParticipatedTierTimeout.t.sol' --match-test test_codexNemesis_singleParticipatedTierZeroTimeoutLocksGame -vvv
[PASS] test_codexNemesis_singleParticipatedTierZeroTimeoutLocksGame() (gas: 2793150)
1 tests passed, 0 failed
PoC sequence:
- Launch a four-tier game with
scorecardTimeout = 0.
- Mint only tier 1 before scoring.
- Move to SCORING.
- Submit the only valid scorecard, assigning all cash-out weight to tier 1.
- Tier 1 holder attests. The call reverts because BWA weight is zero.
- Warp one year. The phase remains SCORING because timeout is disabled.
triggerNoContestFor reverts DefifaDeployer_NotNoContest.
- Cashing out the player token reverts
DefifaHook_NothingToClaim.
Why this survived self-review
Strongest counterargument: the launch guard intentionally allows some two-plus-tier zero-timeout games because they may still reach quorum depending on holder distribution.
Why it fails: the contract cannot enforce that distribution at launch, and the later governance path explicitly switches to runtime supply. Once runtime participation collapses to one tier, neither ratification nor timeout recovery remains reachable.
Additional mitigations checked:
- No hidden no-contest transition exists when
scorecardTimeout == 0.
- Nonzero weights for unowned tiers cannot be used to create nonzero BWA opposition.
- Cash-out without final weights reverts instead of providing a fallback refund.
Recommended fix
Require a nonzero scorecardTimeout for every game unless the contract enforces a stronger runtime participation invariant:
if (launchProjectData.scorecardTimeout == 0) {
revert DefifaDeployer_InvalidGameConfiguration({
start: launchProjectData.start,
mintPeriodDuration: launchProjectData.mintPeriodDuration,
refundPeriodDuration: launchProjectData.refundPeriodDuration,
tierCount: launchProjectData.tiers.length
});
}
Alternatively, make currentGamePhaseOf return NO_CONTEST when scoring has fewer than two eligible participated tiers and no scorecard has been ratified, using the same eligible-tier definition as quorum().
Audit seed
Nemesis fresh round / defifa all
src/**/*.solandscript/**/*.sol/ lifecycle-state coupling, governance, settlement.Repos involved
defifanana-core-v6,nana-721-hook-v6Root cause
DefifaDeployer.launchGameWithonly rejectstiers.length == 1 && scorecardTimeout == 0, but governance reachability is determined later by runtime participated tiers, not configured tier count. A multi-tier game withscorecardTimeout == 0can enter scoring with only one tier having supply. At that point the only valid scorecard assigns 100% cash-out weight to that tier, which gives its holders zero BWA power, whilequorum()still returns a nonzero threshold for the one participated tier.Relevant code:
defifa/src/DefifaDeployer.sol:428-440rejects only one configured tier with zero timeout.defifa/src/DefifaDeployer.sol:261-267only enters NO_CONTEST on timeout whenscorecardTimeout > 0.defifa/src/DefifaGovernor.sol:301-309rejects nonzero scorecard weights for unowned tiers.defifa/src/libraries/DefifaHookLib.sol:444-448requires weights to sum toTOTAL_CASHOUT_WEIGHT.defifa/src/DefifaGovernor.sol:703-707sets BWA multiplier to zero for a 100%-weighted tier.defifa/src/DefifaGovernor.sol:173-177rejects zero-weight attestations.defifa/src/DefifaGovernor.sol:739-759returns a nonzero quorum for one participated tier.defifa/src/DefifaDeployer.sol:646-650prevents no-contest triggering unless the phase already reports NO_CONTEST.defifa/src/DefifaHook.sol:744-751reverts cash-out when no reclaim/fee-token claim exists.Impact
Funds paid into a validly launched multi-tier game can be stuck with no on-chain final settlement or no-contest refund path if only one tier participates before scoring. This can happen naturally in one-sided markets, and a malicious launcher can increase the likelihood by disabling the timeout and configuring unattractive alternative tiers.
Severity: MEDIUM. The impact is a permanent liveness/settlement failure for affected game participants, but it depends on a zero-timeout configuration and one-sided participation.
Proof of concept
A temporary Foundry test was run locally:
PoC sequence:
scorecardTimeout = 0.triggerNoContestForrevertsDefifaDeployer_NotNoContest.DefifaHook_NothingToClaim.Why this survived self-review
Strongest counterargument: the launch guard intentionally allows some two-plus-tier zero-timeout games because they may still reach quorum depending on holder distribution.
Why it fails: the contract cannot enforce that distribution at launch, and the later governance path explicitly switches to runtime supply. Once runtime participation collapses to one tier, neither ratification nor timeout recovery remains reachable.
Additional mitigations checked:
scorecardTimeout == 0.Recommended fix
Require a nonzero
scorecardTimeoutfor every game unless the contract enforces a stronger runtime participation invariant:Alternatively, make
currentGamePhaseOfreturn NO_CONTEST when scoring has fewer than two eligible participated tiers and no scorecard has been ratified, using the same eligible-tier definition asquorum().