Token-0x

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

Integer Overflow in `_transfer` Causes Recipient Balance to Wrap Around

Author Revealed upon completion

Root Cause:

_transfer uses inline assembly and adds value to toAmount without validating that toAmount + value <= type(uint256).max (no overflow guard)

Impact:

Overflow causes recipient balance to wrap around to a small number → recipient loses almost all tokens when receiving a transfer.

Description

  • In _transfer, the recipient balance is updated with a raw add in inline assembly: sstore(toSlot, add(toAmount, value)).

  • If toAmount is close to type(uint256).max, add overflows and wraps to a small value instead of reverting, breaking accounting invariants and enabling loss/manipulation of holder funds.

function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
// ...
let toSlot := keccak256(ptr, 0x40)
let toAmount := sload(toSlot)
// Missing: check that toAmount + value does not overflow
@> sstore(toSlot, add(toAmount, value)) // can overflow when toAmount is near uint256.max
// ...
}
}

Risk

Likelihood:

  • When recipient balance approaches uint256.max

  • During high-inflation token minting periods

  • In whale wallets or system treasury accounts

Impact:

  • Recipient balance wraps → funds effectively lost

  • Total supply invariant breaks

  • Circulating supply becomes inconsistent

  • Governance and reward mechanisms become exploitable

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor() ERC20("Test", "TST") {}
function mint(address to, uint256 amount) public { _mint(to, amount); }
}
contract TransferOverflowTest is Test {
ERC20Mock public token;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
token = new ERC20Mock();
}
function test_transfer_overflow_recipient() public {
uint256 maxUint = type(uint256).max;
uint256 initialBobBalance = maxUint - 100;
token.mint(bob, initialBobBalance);
token.mint(alice, 200);
vm.prank(alice);
token.transfer(bob, 200);
uint256 bobBalance = token.balanceOf(bob);
console.log("Bob before:", initialBobBalance);
console.log("Bob after :", bobBalance);
}
}

Run:

forge test --match-test test_transfer_overflow_recipient -vvvv

Output:

[PASS] test_transfer_overflow_recipient() (gas: 73012)
Logs:
Bob before: 115792089237316195423570985008687907853269984665640564039457584007913129639835
Bob after : 99

Recommended Mitigation

- sstore(fromSlot, sub(fromAmount, value))
+ let newToBalance := add(toAmount, value)
+ // Overflow check: if result is less than original, an overflow occurred
+ if lt(newToBalance, toAmount) {
+ // Revert with standard Panic(0x11) = arithmetic overflow
+ mstore(0x00, shl(224, 0x4e487b71)) // Panic(uint256) selector
+ mstore(add(0x00, 4), 0x11) // panic code 0x11 (overflow)
+ revert(0x00, 0x24)
+ }
+ sstore(toSlot, newToBalance)

Support

FAQs

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

Give us feedback!