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 SushiSwap, Balancer, 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
- Transfer pattern identification: Locates ERC-20
transfer/transferFromcalls by selector matching. - Balance update tracking: Finds SSTORE operations that record the transfer amount.
- Balance measurement check: Verifies whether
balanceOfis called before and after the transfer to compute the actual received amount. - 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
balanceOfresult is actually used in the accounting update — only that it is called.
Related Detectors
- Fee On Transfer — detects fee-on-transfer token incompatibility
- Rebasing Token — detects rebasing token incompatibility
- Unchecked ERC20 — detects unchecked ERC20 return values