Reproduced Exploit
Attacker can stall Forta vault undelegations (locked funds via balance donation)
Chain
Other
Category
logic
Date
Jan 1970
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 32467-attacker-can-stall-undelegations-openzeppelin-none-forta-sta. Standalone Foundry PoC and full write-up: 32467-attacker-can-stall-undelegations-openzeppelin-none-forta-sta_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/incorrect-value-source · vuln/defi/griefing · vuln/math/underflow-dos
Reproduction: the test deploys the REAL, unmodified audited
FortaStakingVault+InactiveSharesDistributor(NethermindEth/forta-staking-vault @ ce87cff) and drives the two-step undelegation to completion. A 1-wei FORT donation to the cloned distributor permanently bricks completion and traps the delegated stake.
Root cause#
Undelegating a subject's stake is a two-step flow:
- The operator calls
FortaStakingVault.initiateUndelegate: it clones anInactiveSharesDistributor, transfers the vault's FortaStaking active shares to it, and arms the FortaStaking withdrawal delay. - After the delay, anyone calls
FortaStakingVault.undelegate, which drivesInactiveSharesDistributor.undelegateto pull the FORT back into the vault.
The distributor computes how much FORT it received the wrong way. FortaStaking.withdraw returns the exact amount released, but the distributor ignores that return value and instead reads its own token balance:
// src/InactiveSharesDistributor.sol:74-75
_staking.withdraw(DELEGATOR_SCANNER_POOL_SUBJECT, _subject);
uint256 assetsReceived = _token.balanceOf(address(this)); // <-- attacker-manipulable
FORT is a plain ERC-20, so anyone can transfer FORT directly to the freshly-cloned distributor. That donation inflates assetsReceived, the distributor forwards the whole (stake + donation) to the vault, and the vault then executes:
// src/FortaStakingVault.sol:259
_assetsPerSubject[subject] -= (afterWithdrawBalance - beforeWithdrawBalance);
_assetsPerSubject[subject] only ever tracked the real staked amount, so subtracting stake + donation underflows and reverts (Solidity 0.8 checked-arithmetic Panic 0x11). Because undelegate is the only path that completes the withdrawal and it now reverts every time, the delegated FORT is stuck as an un-completable withdrawal inside FortaStaking — the vault (and thus its depositors) can never reclaim it. The stall is permanent: the donation already sits in the distributor, so retries keep reverting regardless of elapsed time. The operator can only paper over it by delegating more stake to bump _assetsPerSubject, and an attacker can immediately re-grief.
The fix (PR #24) uses the value returned by withdraw instead of the distributor's balance.
What is real vs modelled#
- Real, unmodified audited source (deployed and executed):
FortaStakingVault,InactiveSharesDistributor,RedemptionReceiver, plus the realIFortaStaking/FortaStakingUtils/OperatorFeeUtilsand the real OpenZeppelin v5 (upgradeable) stack. Vendored undersrc/, byte-identical to commitce87cff. - Modelled minimally: the external FortaStaking core (out of audited scope). The project's own tests reach it only via a Polygon mainnet fork against the deployed
0xd286…6874; for a local, fork-free deploy we use a faithfulMinimalFortaStakingwith real ERC-1155 share accounting, 1:1 stake↔shares (no slashing). Itswithdraw()returns the true released amount — the bug is entirely in the audited contracts trusting a balance instead of that return value.
Reproduction#
_shared/run-poc/run_poc.sh 32467-attacker-can-stall-undelegations-openzeppelin-none-forta-sta_exp -vvvvv
Two tests in test/32467-attacker-can-stall-undelegations-openzeppelin-none-forta-sta_exp.sol:
test_baseline_undelegate_succeeds_without_donation— control: with no interference the vault reclaims its full 100 FORT (_assetsPerSubject → 0, distributor emptied). Isolates the donation as the sole cause.test_attack_donation_permanently_stalls_undelegation— attack: a 1-wei FORT donation makesundelegaterevert with an arithmetic underflow; the vault reclaims0, the 100 FORT stays locked as an un-completable withdrawal in FortaStaking, and a later retry (even 1,000,000 s later) reverts again.
Expected result: 2 passed.
Attack sequence#
Sources#
- AuditVault finding #32467
- Forta Staking Vault repo @ ce87cff
- Vulnerable sources:
src/FortaStakingVault.sol#L233,src/InactiveSharesDistributor.sol#L75 - OpenZeppelin audit report · Fix PR #24
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 32467-attacker-can-stall-undelegations-openzeppelin-none-forta-sta_exp (evm-hack-registry mirror).
- AuditVault finding: 32467-attacker-can-stall-undelegations-openzeppelin-none-forta-sta.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Attacker can stall Forta vault undelegations (locked funds via balance donation)".
- 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.