- Any method could be executed between a method execution and its callback
- The same method could be executed multiple times before the callback completes
- State changes made before the callback can be exploited by malicious actors
Fundamental Assumptions
When designing secure smart contracts, you must assume:- Any method can execute between your method and its callback
- The same method can be re-entered multiple times before the callback executes
- Attackers will exploit any state inconsistencies they can find
- State changes are visible immediately after a method completes, even before callbacks.
Reentrancy Attack Example: deposit_and_stake
Vulnerable Implementation (WRONG)
Consider adeposit_and_stake function with the following flawed logic:
- User sends money to the contract
- Contract immediately adds money to user’s balance (state change)
- Contract makes cross-contract call to stake money in validator
- If staking fails, callback removes the balance.
- Attacker calls
deposit_and_stakewith 10 NEAR - Contract adds 10 NEAR to attacker’s balance (step 2 completes)
- Contract initiates cross-contract call to validator (step 3)
- Before callback executes, attacker calls
withdraw()method - Attacker successfully withdraws 10 NEAR (balance was already updated)
- If staking fails, callback removes balance, but attacker already withdrew
- Result: Attacker receives 10 NEAR, contract loses funds.
Secure Implementation (CORRECT)
The solution is to delay state changes until the callback confirms success:- User sends money to the contract
- Do NOT add to balance yet - store in temporary/pending state
- Contract makes cross-contract call to stake money in validator
- In callback: Only if staking succeeded, then add money to user’s balance
- If staking failed, return money to user (no balance update needed).
Prevention Strategies
1. Delay State Updates
- Never update balances or critical state before cross-contract calls
- Store pending operations in temporary state
- Only commit state changes in callbacks after confirming success.
2. Use Checks-Effects-Interactions Pattern
- Checks: Validate all inputs and preconditions
- Effects: Update state (but only after external calls complete)
- Interactions: Make external calls last.
3. Implement Reentrancy Guards
- Use flags to prevent re-entry during critical operations
- Mark methods as “in progress” during execution
- Clear flags only after all operations complete.
4. Validate in Callbacks
- Always check the result of external calls in callbacks
- Rollback any state changes if external operations failed
- Never assume external calls will succeed.
Best Practices
- Assume reentrancy is possible - design your contract defensively
- Minimize state changes before external calls
- Validate everything in callbacks before committing state
- Test with attack scenarios - simulate reentrancy attempts
- Review callback logic carefully - this is where vulnerabilities hide
- Keep state consistent at all times, even during async operations