Skip to main content
Sigvex

Balance Accounting Mismatch

Detects contracts that assume transfer amounts equal the actual balance change, failing to account for fee-on-transfer, rebasing, or deflationary tokens.

Balance Accounting Mismatch

Overview

The balance accounting detector identifies contracts that record a token transfer amount based on the amount parameter passed to transfer/transferFrom rather than measuring the actual balance change. For standard ERC-20 tokens this works correctly, but for tokens with transfer fees, rebasing mechanics, or deflationary burns, the recorded amount differs from the received amount.

Why This Is an Issue

An attacker can deposit a fee-on-transfer token, receive credit for the full amount, then withdraw more than the contract actually holds. The contract’s internal accounting overestimates its balance, leading to insolvency for the last withdrawers. This pattern has caused losses across multiple DeFi protocols including a major AMM, a major weighted-pool AMM, and numerous yield aggregators. The total losses from fee-on-transfer accounting bugs exceed $50M.

How to Resolve

// Before: Assumes amount sent equals amount received
function deposit(IERC20 token, uint256 amount) external {
    token.transferFrom(msg.sender, address(this), amount);
    balances[msg.sender] += amount; // Wrong for fee tokens
}

// After: Measure actual balance change
function deposit(IERC20 token, uint256 amount) external {
    uint256 balanceBefore = token.balanceOf(address(this));
    token.transferFrom(msg.sender, address(this), amount);
    uint256 received = token.balanceOf(address(this)) - balanceBefore;
    balances[msg.sender] += received; // Correct for all token types
}

Examples

Vulnerable Code

contract VulnerableVault {
    mapping(address => mapping(address => uint256)) public deposits;

    function deposit(address token, uint256 amount) external {
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        deposits[msg.sender][token] += amount; // Overestimates for fee tokens
    }

    function withdraw(address token, uint256 amount) external {
        require(deposits[msg.sender][token] >= amount);
        deposits[msg.sender][token] -= amount;
        IERC20(token).transfer(msg.sender, amount); // May exceed actual balance
    }
}

Fixed Code

contract SafeVault {
    mapping(address => mapping(address => uint256)) public deposits;

    function deposit(address token, uint256 amount) external {
        uint256 before = IERC20(token).balanceOf(address(this));
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        uint256 received = IERC20(token).balanceOf(address(this)) - before;
        deposits[msg.sender][token] += received;
    }

    function withdraw(address token, uint256 amount) external {
        require(deposits[msg.sender][token] >= amount);
        deposits[msg.sender][token] -= amount;
        uint256 before = IERC20(token).balanceOf(address(this));
        IERC20(token).transfer(msg.sender, amount);
        uint256 sent = before - IERC20(token).balanceOf(address(this));
        require(sent <= amount, "Unexpected balance change");
    }
}

Sample Sigvex Output

{
  "detector_id": "balance-accounting",
  "severity": "high",
  "confidence": 0.90,
  "description": "Function deposit() records transfer amount parameter (slot 2) as balance credit without measuring actual balance change via balanceOf(). For fee-on-transfer or rebasing tokens, this causes accounting mismatch leading to insolvency.",
  "location": { "function": "deposit(address,uint256)", "offset": 96 }
}

Detection Methodology

  1. Transfer pattern identification: Locates ERC-20 transfer/transferFrom calls by selector matching.
  2. Balance update tracking: Finds SSTORE operations that record the transfer amount.
  3. Balance measurement check: Verifies whether balanceOf is called before and after the transfer to compute the actual received amount.
  4. Taint analysis: Confirms that the stored balance value flows from the transfer parameter rather than from the balance delta.

Limitations

  • Contracts that intentionally restrict to known non-fee tokens (via allowlists) will still be flagged.
  • Wrapped token interactions where the wrapper guarantees 1:1 transfer may produce false positives.
  • The detector does not verify that the balanceOf result is actually used in the accounting update – only that it is called.

References