The vulnerable pattern
Imagine that we develop awithdraw method with the following wrong logic:
- We send tokens to the user
- On success, we deduct the amount from the user’s balance
Pseudocode: vulnerable flow
How the exploit happens
- The user calls
withdrawand the contract sends the tokens to the user - The callback is scheduled, but the user can call
withdrawagain before the callback runs! - The user calls
withdrawagain, or multiple times as a batched transaction, the contract keeps sending tokens to the user - The callback runs and deducts the amount from the user’s balance only once, leaving the user with more tokens than they should have.
The safe pattern
Deduce the user’s balance before sending the tokens! Anyway if the transfer fails, you can always refund the user in the callback.Pseudocode: safe flow
The opposite rule for deposits
For a deposit whose benefit depends on a cross-contract call, do not add the user’s balance first and deduct it again if the call fails:Pseudocode: vulnerable deposit flow
deposit_callback runs, the user can withdraw the balance that was already credited. If staking then fails, the callback also refunds the deposit.
Instead, add the balance only after the call succeeds:
Pseudocode: safe deposit flow
- Withdrawals: deduct the balance before the call; add it back only if the call fails.
- Deposits: add the balance only if the call succeeds.