Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: high
Likelihood: high

Integer Overflow in _transfer

Author Revealed upon completion

Integer Overflow in _transfer

Root + Impact

Description

  • Normal ERC20 transfers should revert if the recipient's balance overflows the maximum uint256 value.

  • The _transfer function in ERC20Internals.sol uses assembly to perform arithmetic operations without checking for overflow, allowing a recipient's balance to wrap around.

// src/helpers/ERC20Internals.sol
function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
// ... (checks for zero address)
// ... (load balances)
if lt(fromAmount, value) {
// ... (revert if insufficient balance)
}
sstore(fromSlot, sub(fromAmount, value))
sstore(toSlot, add(toAmount, value)) // @> No check for overflow here
success := 1
// ...
}
}

Risk

Likelihood:

  • High // Any user can transfer tokens to another user (or themselves) to trigger this if they have enough tokens to cause an overflow when added to the recipient's balance.

Impact:

  • High// A user's balance can be manipulated to wrap around to a small number, or if exploited carefully, could disrupt token accounting.

  • High// Violates the ERC20 standard invariant that total supply matches the sum of balances (if totalSupply doesn't track this wrap-around, though totalSupply isn't updated in transfer).

Proof of Concept

function test_Overflow_Transfer() public {
address userA = makeAddr("userA");
address userB = makeAddr("userB");
// Mint large amounts
uint256 initialBalance = type(uint256).max / 2 + 100;
token.mint(userA, initialBalance);
token.mint(userB, initialBalance);
// Transfer from A to B to cause overflow in B's balance
uint256 transferAmount = initialBalance;
vm.prank(userA);
token.transfer(userB, transferAmount);
// B's balance overflows and wraps around
unchecked {
assertEq(token.balanceOf(userB), initialBalance + transferAmount);
}
}

Recommended Mitigation

- sstore(toSlot, add(toAmount, value))
+ let newToAmount := add(toAmount, value)
+ // Check for overflow: if newToAmount < toAmount, it wrapped around
+ if lt(newToAmount, toAmount) { revert(0, 0) }
+ sstore(toSlot, newToAmount)

The mitigation involves manually checking for overflow after the addition. In Yul, add(a, b) wraps on overflow. By checking if the result is less than one of the operands (e.g., newToAmount < toAmount) we can detect if an overflow occurred and revert the transaction.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!