Skip to main content
Between a cross-contract call and its callback, any public method of your contract can execute. This means that, particularly, a same method can also execute again before the callback runs. If you forget to deduct a user’s balance before sending them tokens, they can call the same method again and again, draining your contract of tokens.

The vulnerable pattern

Imagine that we develop a withdraw method with the following wrong logic:
  1. We send tokens to the user
  2. On success, we deduct the amount from the user’s balance
Pseudocode: vulnerable flow

How the exploit happens

  1. The user calls withdraw and the contract sends the tokens to the user
  2. The callback is scheduled, but the user can call withdraw again before the callback runs!
  3. The user calls withdraw again, or multiple times as a batched transaction, the contract keeps sending tokens to the user
  4. 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
Now a withdrawal between the call and callback finds no new balance to withdraw!

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
Before 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
The two rules are mirrors of each other:
  • Withdrawals: deduct the balance before the call; add it back only if the call fails.
  • Deposits: add the balance only if the call succeeds.