Reproduced Exploit

FactoryDAO — Blacklisted user may permanently block `withdrawExcessRewards`

1. withdraw AND-chains every ERC20 transfer and reverts if any fails. 2. A blacklisted (or paused-token) user can never clear their deposit. 3. withdrawExcessRewards requires totalDepositsWei == 0. 4. HARM in the PoC: attacker deposits 1 wei, gets blacklisted; after honest users leave, owner excess…

May 2022Otheruntagged3 min read

Chain

Other

Category

untagged

Date

May 2022

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 42551-h-02-dos-blacklisted-user-may-prevent-withdrawexcessrewards. Standalone Foundry PoC and full write-up: 42551-h-02-dos-blacklisted-user-may-prevent-withdrawexcessrewards_exp in the evm-hack-registry mirror.


Vulnerability classes: impact/frozen-funds · impact/dos · genome/reward-accounting · dos-resistance

Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only forge-std — no fork, no RPC, no anvil_state. Full trace: output.txt. PoC: test/42551-h-02-dos-blacklisted-user-may-prevent-withdrawexcessrewards_exp.sol.

AuditVault taxonomy: lang/solidity · platform/code4rena · has/github · has/poc · severity/high · sector/token · genome: frozen-funds · locked-funds · dos-resistance · reward-accounting


Key info#

ImpactHIGH — one non-receivable depositor (e.g. blacklisted) can never withdraw(), so totalDepositsWei never hits 0 and owner excess rewards lock forever
ProtocolFactoryDAOPermissionlessBasicPoolFactory
Vulnerable codewithdraw transfer loop + withdrawExcessRewards totalDepositsWei == 0 gate
Bug classPush-payment DoS coupled to global excess-reward gate
FindingCode4rena — FactoryDAO, 2022-05 · #42551 (H-02) · reporter IllIllI / AuditsAreUS
Report2022-05-factorydao
SourceAuditVault
StatusAudit finding — confirmed and fixed. Local synthetic reproduces the lock.
Compiler^0.8.24 (PoC)

TL;DR#

  1. withdraw AND-chains every ERC20 transfer and reverts if any fails.
  2. A blacklisted (or paused-token) user can never clear their deposit.
  3. withdrawExcessRewards requires totalDepositsWei == 0.
  4. HARM in the PoC: attacker deposits 1 wei, gets blacklisted; after honest users leave, owner excess rewards remain locked in the factory.

The vulnerable code#

From code-423n4/2022-05-factorydao@db41580 PermissionlessBasicPoolFactory.sol:

SOLIDITY
for (uint i = 0; i < rewards.length; i++) {
    ...
    success = success && IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount); // @> VULN
}
success = success && IERC20(pool.depositToken).transfer(receipt.owner, receipt.amountDepositedWei);
require(success, 'Token transfer failed');

// withdrawExcessRewards:
require(pool.totalDepositsWei == 0, 'Cannot withdraw until all deposits are withdrawn'); // @> VULN

Fix: allow excess withdrawal after pool end / timeout, or use try/catch + pull payments so one bad recipient cannot brick the pool.


Root cause#

A local per-user transfer failure is coupled to a global owner withdrawal precondition. The cost to an attacker is negligible (1 wei deposit + becoming non-receivable).


Preconditions#

  • Pool with deposit + reward tokens that can blacklist or fail transfers.
  • Attacker can deposit a tiny amount and become non-receivable on at least one transferred token.

Attack walkthrough#

  1. Honest user deposits 50e18; attacker deposits 1 wei; rewards funded.
  2. Attacker is blacklisted on the reward token.
  3. Honest withdraw succeeds; attacker withdraw reverts on failed transfer.
  4. totalDepositsWei == 1 forever → withdrawExcessRewards reverts → excess rewards stuck.

Diagrams#

flowchart TD A["Attacker deposits 1 wei"] --> B["Attacker blacklisted on reward token"] B --> C["Honest users withdraw"] C --> D["Attacker withdraw reverts on transfer"] D --> E["totalDepositsWei never 0"] E --> F["withdrawExcessRewards reverts forever"] F --> G["HARM: owner excess rewards locked"]
sequenceDiagram participant H as Honest participant A as Attacker participant F as PoolFactory participant R as RewardToken H->>F: deposit 50e18 A->>F: deposit 1 wei A->>R: setBlacklist attacker H->>F: withdraw OK A->>F: withdraw F->>R: transfer attacker rewards R-->>F: false F-->>A: revert Token transfer failed Note over F: totalDepositsWei still 1 F-->>F: withdrawExcessRewards reverts

Remediation#

DIFF
- require(pool.totalDepositsWei == 0, 'Cannot withdraw until all deposits are withdrawn');
+ // allow after pool end timestamp, or skip failed recipients via try/catch pull pattern

How to reproduce#

BASH
cd ~/RustroverProjects/audits/evm-hack-registry/42551-h-02-dos-blacklisted-user-may-prevent-withdrawexcessrewards_exp
forge test -vvv

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.