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.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.