When a staker attempts to double-claim via claimSurvived() or claimExpired(), both functions revert with InvalidAmount() — the same error used for zero-amount stake inputs. The semantically correct response is a distinct AlreadyClaimed error. This inconsistency misleads off-chain integrators, DeFi aggregators, and user interfaces that route on error selectors. Additionally, the soft-success path in claimExpired() (for zero-stake callers) never sets hasClaimed, allowing unlimited repeated calls post-resolution.
InvalidAmount() is documented and used elsewhere to mean "you passed 0 or a below-minimum value to a deposit function." Reusing it for "you already claimed your tokens" breaks the ABI contract with callers who decode errors by selector.
Solidity custom errors are the primary signalling mechanism between smart contracts and their off-chain consumers (SDKs, frontends, bots). Each error code carries an implicit semantic contract: InvalidAmount means the input value is wrong. Using it to signal state (already claimed) produces a false positive in any error-routing logic that tries to distinguish "user made an input mistake" from "user already redeemed."
A DeFi aggregator that retries on InvalidAmount (interpreting it as a transient input issue) would submit the same claim transaction indefinitely, burning user gas. A frontend that displays the error string for InvalidAmount as "invalid amount" tells the user nothing about what actually happened.
The claimAttackerBounty function handles its equivalent case correctly with BountyAlreadyClaimed(), demonstrating that the protocol team had the right idea — it was simply not applied consistently.
The soft-success issue is a secondary bug in the same area: a non-staker who calls claimExpired() to trigger mechanical pool resolution (a legitimate use-case) has their hasClaimed flag never written. Post-resolution, that address can call claimExpired() in a tight loop (all calls will soft-succeed with no state change). While there is no financial loss, it burns unnecessary gas and is confusing to monitor.
For double-claim revert: hasClaimed[msg.sender] == true (staker has already claimed once).
For soft-success repetition: outcome != UNRESOLVED (pool resolved), eligibleStake[msg.sender] == 0 (caller is not a staker).
None — these are pure behavioral defects triggered by normal user interaction patterns.
Double-claim wrong selector:
Staker calls claimSurvived() — succeeds, hasClaimed[alice] = true.
Staker calls claimSurvived() again (accidentally or in a retry loop).
Reverts with InvalidAmount() — integrator displays "invalid amount entered" to user instead of "already claimed."
User is confused; integrator's error handler may retry the transaction, burning further gas.
Soft-success unbounded repetition:
Non-staker (address with eligibleStake == 0) calls claimExpired() post-expiry.
Pool resolves to EXPIRED; claimsStarted = true; function returns early at line 584.
hasClaimed[caller] is never written to true.
Same caller repeats call — enters the already-resolved branch (line 518 skipped), hits line 578 (hasClaimed == false, no revert), hits line 581 (userEligible == 0, returns again).
This cycle can repeat indefinitely with no on-chain consequence other than gas cost.
No loss of funds in either sub-case. Impact is:
Integrator breakage: error selector mismatch causes incorrect UX rendering and potentially incorrect retry logic.
Gas waste: soft-success repetition burns gas on every call with no benefit.
Inconsistent ABI: BountyAlreadyClaimed exists for bounty claims but not for staker claims, creating an uneven developer experience.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.