The Staking contract allows attackers to deposit on behalf of any address and each deposit creates a new locked Stake entry in a per-user array. An attacker can spam thousands of tiny deposits for a victim so the victim’s stakes[victim] array becomes huge. Both normal withdraw and the emergency withdraw flows iterate that array; when it’s big enough the calls will run out of gas and revert. Result: victim’s funds become permanently locked.
Vulnerability Details
Anyone can deposit on behalf of any address in deposit() from ERC4626:
functiondeposit(uint256assets,addressto)publicvirtualreturns(uint256shares){if(assets >maxDeposit(to))_revert(0xb3c61a83);// `DepositMoreThanMax()`. shares =previewDeposit(assets);_deposit(msg.sender, to, assets, shares);}
From _deposit() every deposit pushes a new stake entry:
The _consumeUnlockedSharesOrRevert() iterates the whole array:
The _removeAnySharesFor() also iterates the whole array:
1
Attack flow — step-by-step
Attacker calls deposit(1, victim) many times. stakes[victim] grows to N entries.
Victim calls deposit(100e18, victim). This pushes a new Stake entry into the array.
Attacker spam deposits again M times to same victim. Victim’s honest deposit entry ends up sandwiched inside a much larger array.
Effect on withdraws:
Normal withdraw uses _consumeUnlockedSharesOrRevert() which reads the whole stakes array and will revert due to out of gas when the array is large enough.
Emergency withdraw calls _removeAnySharesFor, which also iterates and performs swap-and-pop; it too will revert when the array is large enough.
Impact Details
Critical: attacker can permanently block a user’s ability to withdraw their tokens at low attack cost (small deposits + gas).
Proof of Concept
Click to expand PoC (Forge test)
Note: for simplicity, the PoC uses the Staking contract directly (not via proxy — comment the constructor in Staking.sol).
Run the test with: forge test --fork-url https://bsc-dataseed1.binance.org