Token-0x

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

Uint256 Overflow in ERC20Internals.sol::_transfer Causes Recipient Balance Corruption

Author Revealed upon completion

Uint256 Overflow in ERC20Internals.sol::_transfer Causes Recipient Balance Corruption

Description

  • In assembly, add wraps on overflow (unlike Solidity 0.8+ checked arithmetic). If toAmount + value > type(uint256).max, the result wraps.

  • When an integer wraps the user will experience an incorrect balance and potential token loss.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ERC20Internals {
mapping(address account => uint256) internal _balances;
mapping(address account => mapping(address spender => uint256)) internal _allowances;
uint256 internal _totalSupply;
string internal _name;
string internal _symbol;
// ...rest of code...
function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
if iszero(from) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
if iszero(to) {
mstore(0x00, shl(224, 0xec442f05))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let baseSlot := _balances.slot
mstore(ptr, from)
mstore(add(ptr, 0x20), baseSlot)
let fromSlot := keccak256(ptr, 0x40)
let fromAmount := sload(fromSlot)
mstore(ptr, to)
mstore(add(ptr, 0x20), baseSlot)
let toSlot := keccak256(ptr, 0x40)
let toAmount := sload(toSlot)
if lt(fromAmount, value) {
mstore(0x00, shl(224, 0xe450d38c))
mstore(add(0x00, 4), from)
mstore(add(0x00, 0x24), fromAmount)
mstore(add(0x00, 0x44), value)
revert(0x00, 0x64)
}
sstore(fromSlot, sub(fromAmount, value))
@> sstore(toSlot, add(toAmount, value)) // this addition can cause an integer overflow
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}
// ...rest of code...
}

Risk

Likelihood:

  • This occurs during any transfer() or transferFrom() call where the recipient's existing balance is sufficiently large that adding the transfer amount would overflow the uint256 type.

Impact:

  • Balance corruption breaks token accounting integrity, potentially causing financial losses and making the contract's state inconsistent.

Proof of Concept

  1. Add test_transferOverflow to the Token.T.sol test file

  2. Run with command forge test --match-test test_transferOverflow -vv

function test_transferOverflow() public {
address sender = makeAddr("sender");
address recipient = makeAddr("recipient");
// Set recipient balance to near max uint256
// type(uint256).max = 2^256 - 1
uint256 maxUint = type(uint256).max;
uint256 recipientInitialBalance = maxUint - 100; // Leave room for overflow test
uint256 transferAmount = 200; // This will cause overflow
// Mint tokens to sender (they need enough to transfer)
token.mint(sender, transferAmount);
// Calculate the storage slot for _balances[recipient]
// _balances is the first state variable in ERC20Internals, so it's at slot 0
// The slot calculation matches what's done in _transfer: keccak256(recipient, baseSlot)
bytes32 baseSlot = bytes32(uint256(0)); // _balances mapping is at slot 0
bytes32 recipientBalanceSlot = keccak256(abi.encode(recipient, baseSlot));
// Set recipient balance to near max using vm.store
vm.store(address(token), recipientBalanceSlot, bytes32(recipientInitialBalance));
// Verify recipient has the expected balance
uint256 balanceBefore = token.balanceOf(recipient);
assertEq(balanceBefore, recipientInitialBalance, "Recipient should have near-max balance");
// Transfer amount that will cause overflow
// This should revert but won't due to missing overflow check
vm.prank(sender);
token.transfer(recipient, transferAmount);
// Check the balance after transfer
uint256 balanceAfter = token.balanceOf(recipient);
// If overflow occurred, balance will wrap around
// Expected: recipientInitialBalance + transferAmount = maxUint - 100 + 200 = maxUint + 100
// But maxUint + 100 wraps to: (maxUint + 100) % (2^256) = 99
uint256 expectedWrappedBalance = 99; // (maxUint - 100 + 200) % (2^256) = 99
// This proves the overflow: the balance wrapped around instead of reverting
assertEq(balanceAfter, expectedWrappedBalance, "Balance overflowed and wrapped around");
assertLt(balanceAfter, recipientInitialBalance, "Balance decreased due to overflow wrap");
// The correct behavior would be to revert, but instead we get a wrapped value
// This demonstrates the vulnerability: tokens are effectively lost/corrupted
}

Recommended Mitigation

Add an overflow check that will revert if the new balance overflows.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ERC20Internals {
mapping(address account => uint256) internal _balances;
mapping(address account => mapping(address spender => uint256)) internal _allowances;
uint256 internal _totalSupply;
string internal _name;
string internal _symbol;
// ...rest of code...
function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
if iszero(from) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
if iszero(to) {
mstore(0x00, shl(224, 0xec442f05))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let baseSlot := _balances.slot
mstore(ptr, from)
mstore(add(ptr, 0x20), baseSlot)
let fromSlot := keccak256(ptr, 0x40)
let fromAmount := sload(fromSlot)
mstore(ptr, to)
mstore(add(ptr, 0x20), baseSlot)
let toSlot := keccak256(ptr, 0x40)
let toAmount := sload(toSlot)
if lt(fromAmount, value) {
mstore(0x00, shl(224, 0xe450d38c))
mstore(add(0x00, 4), from)
mstore(add(0x00, 0x24), fromAmount)
mstore(add(0x00, 0x44), value)
revert(0x00, 0x64)
}
+ let newToAmount := add(toAmount, value)
+ if lt(newToAmount, toAmount) {
+ revert(0, 0)
+ }
sstore(fromSlot, sub(fromAmount, value))
sstore(toSlot, add(toAmount, value))
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}
// ...rest of code...
}

Support

FAQs

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

Give us feedback!