Token-0x

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

_transfer Function Lacks Overflow Check for Receiver Balance Leading to Asset Loss

Author Revealed upon completion

_transfer Function Lacks Overflow Check for Receiver Balance Leading to Asset Loss

Description

In the ERC20 token's _transfer function, it is crucial to ensure the receiver's new balance does not overflow during addition operations for secure transfers.
However, this function directly uses Yul's add instruction to increase the receiver's balance without checking for potential overflow beyond type(uint256).max.
If the receiver's current balance plus the transfer amount exceeds type(uint256).max, it will cause an integer wraparound, setting the receiver's balance to an extremely small value and compromising account asset integrity.

function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
// ...
let toAmount := sload(toSlot)
if lt(fromAmount, value) { revert(...) }
sstore(fromSlot, sub(fromAmount, value))
@> sstore(toSlot, add(toAmount, value)) // No overflow check for receiver's balance
success := 1
// ...
}
}

Risk

Likelihood:

  • Overflows may occur when receiver balances approach type(uint256).max and receive substantial amounts

  • High-inflation models or specific business logic (e.g., reward distributions) increase probability

Impact:

  • Receiver balances may be incorrectly set to minimal values, causing fund loss and trust erosion

  • May trigger cascading logic failures in balance-dependent systems (staking, lending, etc.)

  • Compromises core token accounting integrity

Proof of Concept

  • Add test_1_transfer_overflow to Token.t.sol:

function test_1_transfer_overflow() public {
address user1 = makeAddr("user1");
token.mint(user1, 1);
address user2 = makeAddr("user2");
token.mint(user2, type(uint256).max);
vm.prank(user1);
token.transfer(user2, 1);
assertEq(token.balanceOf(user1), 0);
assertEq(token.balanceOf(user2), 0);
}
  • Execute test: forge test --mt test_1_transfer_overflow -vv

Ran 1 test for test/Token.t.sol:TokenTest
[PASS] test_1_transfer_overflow() (gas: 46828)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.00ms (385.20µs CPU time)

Recommended Mitigation

Add overflow protection before updating receiver balance:

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) {
+ mstore(0x00, shl(224, 0x01))
+ mstore(add(0x00, 4), to)
+ mstore(add(0x00, 0x24), toAmount)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
sstore(fromSlot, sub(fromAmount, value))
- sstore(toSlot, add(toAmount, value))
+ sstore(toSlot, newToAmount)
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}

Support

FAQs

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

Give us feedback!