Root cause: claimSurvived(), claimExpired(), and withdraw() unconditionally send tokens to msg.sender with no alternative-address mechanism.
Direct impact:
Blacklisted staker's principal (up to the full pool value) is permanently locked inside the contract.
totalEligibleStake never reaches zero, so sweepUnclaimedBonus() is permanently blocked — the entire remaining bonus reserve is also unrecoverable.
No owner mitigation exists:
setRecoveryAddress() only redirects claimCorrupted() / sweepUnclaimedCorrupted() / sweepUnclaimedBonus() — it has no effect on individual staker claims.
There is no rescueStuck(), claimFor(), or equivalent admin function.
withdraw() is already disabled post-resolution (outcome != UNRESOLVED check), so the pre-resolution exit path is also closed.
All three staker-exit functions hardcode msg.sender as the ERC-20 recipient with no fallback:
stakeToken is immutable after initialize(). There is no claimTo(address recipient) variant, no pull-payment registry, and no admin function that can touch eligibleStake[specificUser].
When a safeTransfer(msg.sender, ...) call reverts (blacklisted recipient), the entire transaction reverts atomically. Because Solidity's checked arithmetic makes every state mutation in the call stack roll back, eligibleStake[staker] is never decremented and totalEligibleStake is never reduced. The staker cannot retry with a different address; the protocol has no mechanism to redirect the payout.
The secondary lockup follows from the sweepUnclaimedBonus() reserve calculation:
While the blacklisted staker's eligibleStake keeps totalEligibleStake > 0, reserved always equals or exceeds freeBalance, so amount == 0 and the sweep reverts permanently.
Likelihood:
The pool's token allowlist is controlled by the factory owner, meaning only tokens explicitly approved will be used. However the protocol targets multi-year operation (30-day minimum expiry, no upper bound beyond uint32 max), and the README lists no token restrictions beyond "standard ERC-20, no fee-on-transfer, no rebasing." USDC and USDT — the most common DeFi collateral tokens — both implement owner-controlled blacklists. Over a 2–3 year pool lifetime the probability of at least one participant being sanctioned or exchange-frozen is non-trivial, especially for pools with many stakers. The blacklisting event is entirely outside the protocol's control.
Impact:
A staker whose address is blacklisted by a centralized ERC-20 token (e.g. USDC, USDT) after depositing permanently loses their full principal. Every claim attempt reverts atomically, leaving eligibleStake[staker] set forever with no alternative recipient path and no admin escape hatch. As a secondary effect, while the blacklisted stake remains in the accounting the entire remaining bonus reserve is also permanently trapped — sweepUnclaimedBonus() is blocked because reserved >= freeBalance for as long as totalEligibleStake > 0.
Run with: forge test --match-path "test/exploits/ExploitBlacklistStakerFundLock.t.sol" -vvv
Test file (test/exploits/ExploitBlacklistStakerFundLock.t.sol):
The test deploys a BlacklistableERC20 (mimicking USDC's blacklist), runs a two-staker SURVIVED pool through a full risk window, has Bob claim successfully, blacklists Alice, then asserts:
claimSurvived() reverts with BLACKLISTED.
eligibleStake[alice] remains at 100e18 — principal is never decremented.
sweepUnclaimedBonus() reverts with NothingToSweep — bonus reserve is blocked.
setRecoveryAddress(newAddr) + retry still reverts — no owner escape hatch.
Add an optional alternative-recipient path so a staker can redirect their payout to an address that is not blacklisted:
Or, for a simpler fix, add a pre-registered rescue address:
Then in each claim function, resolve the recipient as rescueAddress[msg.sender] != address(0) ? rescueAddress[msg.sender] : msg.sender before the transfer.
Either approach preserves the CEI pattern and requires no changes to the accounting logic.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.