A user should only be able to withdraw their deposited assets once, either by using the standard redeem function or the custom cancelParticipation function.
The contract maintains two parallel and un-synced accounting systems :
The ERC4626 system, which tracks ownership via shares.
The Custom system, which tracks deposits via the stakedAsset mapping. The standard redeem function burns shares and returns assets, but does not update the stakedAsset mapping. A user can call redeem to get their money back, and then call cancelParticipation, which only checks the stakedAsset mapping (which is still full) and sends the assets a second time.
Likelihood:
The attack is permissionless for any user who has deposited. This is a deliberate, not an action a legitimate user would accidentally take.
It relies on an attacker knowing the contract inherits ERC-4626 and has a standard redeem function. This is trivial to discover by reading the import statements or viewing the contract's ABI on a block explorer.
Impact:
An attacker can deposit funds, redeem them, and then call cancelParticipation to redeem the same amount again. They effectively double their money, stealing the funds directly from other legitimate depositors in the vault.
These stolen assets belong to the other legitimate depositors. The attacker's action directly drains the underlying assets, causing the balanceOf(vault) to decrease while totalSupply does not (or not proportionally). This causes the value of all remaining shares to plummet, potentially to zero, as the assets backing their shares have been stolen.
This PoC (test_doubleBurn) demonstrates the double-withdrawal attack.
user1 deposits 20 ether. stakedAsset[user1] and balanceOf(user1) are both set to ~19.7 (20 - 1.5% fee).
user1 calls redeem for all their shares. They receive ~19.7 ether back, and their balanceOf(user1) becomes 0. stakedAsset[user1] remains unchanged.
user1 calls cancelParticipation. The function reads stakedAsset[user1] (~19.7), transfers that amount to them again, and then burns their 0 shares (which does nothing).
user1 has successfully withdrawn ~39.4 ether after only depositing 20.
The cancelParticipation function must be tied to the actual shares a user holds, not the stale stakedAsset mapping. The refund amount should be calculated from the shares being burned, making it ERC4626-compliant.
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.