Skip to main content
Sigvex

msg.value in Loop

Detects msg.value used inside loops where the same ETH value is counted multiple times, enabling fund theft or accounting errors.

msg.value in Loop

Overview

The msg.value-in-loop detector identifies contracts that reference msg.value inside a loop body. Since msg.value is constant throughout a transaction, using it inside a loop that processes multiple items means the same ETH amount is credited to each iteration. An attacker sending 1 ETH can have it counted N times if the loop runs N iterations, effectively multiplying their deposit.

Why This Is an Issue

This is a straightforward accounting flaw. If a batch deposit function loops over recipients and credits each with msg.value, the contract believes it received N times the actual ETH sent. The attacker deposits once but receives credit for N deposits, allowing them to withdraw far more than they sent. This pattern has appeared in multi-call contracts, batch minting functions, and aggregator protocols.

How to Resolve

// Before: Vulnerable — msg.value counted per iteration
function batchDeposit(address[] calldata recipients) external payable {
    for (uint i = 0; i < recipients.length; i++) {
        deposits[recipients[i]] += msg.value;  // Same msg.value each iteration
    }
}

// After: Fixed — divide msg.value or use explicit amounts
function batchDeposit(address[] calldata recipients, uint256[] calldata amounts) external payable {
    uint256 total = 0;
    for (uint i = 0; i < recipients.length; i++) {
        deposits[recipients[i]] += amounts[i];
        total += amounts[i];
    }
    require(total == msg.value, "Amount mismatch");
}

Detection Methodology

  1. Loop identification: Detects loop structures (back-edges in the CFG) in the contract bytecode.
  2. CALLVALUE in loop body: Checks whether the CALLVALUE opcode (which reads msg.value) appears within loop-body blocks.
  3. Accumulation pattern: Flags cases where the CALLVALUE result feeds into an addition or storage write that executes on each iteration.
  4. Single-iteration filter: Loops that provably execute exactly once (e.g., length-1 arrays) are filtered out.

Limitations

False positives: Loops that read msg.value for comparison (e.g., require(msg.value >= minPrice)) without accumulating it are safe but may be flagged. False negatives: Indirect references to msg.value through local variables assigned before the loop are not traced into the loop body.

References