Skip to content

Async Withdraw Race Condition

id: LS08H
title: Async Withdraw Race Condition 
baseSeverity: H
category: race-condition
language: solidity
blockchain: [ethereum]
impact: Multiple withdrawals from the same balance before state update
status: draft
complexity: medium
attack_vector: external
mitigation_difficulty: medium
versions: [">=0.4.0", "<0.8.21"]
cwe: CWE-362
swc: SWC-107

πŸ“ Description

  • Double Spend via Async Withdraw refers to a race condition vulnerability where a user (or attacker) is able to call a withdraw() function multiple times before their balance is updated, especially when external calls (e.g., transfer(), call()) are made before state updates.
  • This results in multiple successful withdrawals from the same balance or claim allocation.
  • This often occurs in poorly ordered code in withdraw() or claim() functions, particularly when gas forwarding or reentrancy opportunities exist.

🚨 Vulnerable Code

contract AsyncVault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Nothing to withdraw");

        // ❌ External call happens before balance update
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Transfer failed");

        balances[msg.sender] = 0; // ❌ State update after call
    }
}

πŸ§ͺ Exploit Scenario

Step-by-step exploit process:

  1. Attacker deposits 1 ETH into AsyncVault.
  2. They call withdraw(), which sends funds via call() before setting balance to 0.
  3. The fallback function in attacker’s contract re-enters withdraw(), repeating the process.
  4. Attacker drains funds multiple times, exceeding their original balance.

Assumptions:

  • Contract uses external calls before updating internal state.
  • No reentrancy guard or state-locking mechanism is in place.

βœ… Fixed Code

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Nothing to withdraw");

        balances[msg.sender] = 0; // βœ… State updated before external call
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Transfer failed");
    }
}

🧭 Contextual Severity

- context: "Async withdrawal logic exposed to reentrancy"
  severity: H
  reasoning: "Leads to total fund loss if attacker loops reentry."
- context: "Claim function is rate-limited or uses delayed execution"
  severity: M
  reasoning: "Timing mitigates mass exploitation, but race still exists."
- context: "State locked before external calls"
  severity: I
  reasoning: "Pattern correctly mitigated, not exploitable."

πŸ›‘οΈ Prevention

Primary Defenses

  • Always follow the Checks-Effects-Interactions pattern.
  • Set balances or state before performing external calls.
  • Use ReentrancyGuard (mutex lock) for functions that move ETH or tokens.

Additional Safeguards

  • Emit withdrawal events after success for off-chain monitoring.
  • Consider using pull payment models (withdraw() pattern) rather than send() or push payments.
  • Harden fallback functions and avoid sending gas-forwarding call() unless necessary.

Detection Methods

  • Slither: reentrancy-eth, dos, and external-call-before-state-change detectors.
  • Manual inspection of withdraw(), claim(), or transfer() logic.
  • Fuzz testing with reentrant contracts to simulate race conditions.

πŸ•°οΈ Historical Exploits

  • Name: Lendf.Me Exploit
  • Date: 2020-04-19
  • Loss: Approximately $25 million
  • Post-mortem: Link to post-mortem

πŸ“š Further Reading


βœ… Vulnerability Report

id: LS08H
title: Async Withdraw Race Condition 
severity: H
score:
impact: 5         
exploitability: 4 
reachability: 4   
complexity: 2     
detectability: 4  
finalScore: 4.2

πŸ“„ Justifications & Analysis

  • Impact: Enables attackers to extract more than their rightful balance.
  • Exploitability: Reentrancy through fallback or low-level call is common and easy to deploy.
  • Reachability: Very common in financial contracts with external withdrawals.
  • Complexity: Simple logic errorβ€”attacker can act with basic tooling.
  • Detectability: Readily flagged by tools like Slither and through code review.