Token-0x

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

M03. Unchecked addition in `_transfer` allows overflow

Author Revealed upon completion

Root + Impact

Unchecked addition in _transfer allows receiver balance overflow, corrupting balances and breaking ERC20 invariants

Description

In a correct ERC20 implementation, a transfer must decrease the sender’s balance and increase the receiver’s balance by the transferred amount. Both operations must preserve arithmetic safety: if the receiver’s balance is already at the maximum uint256 value, adding any positive amount must revert.

In this contract, _transfer is implemented in inline assembly and uses a raw add operation to update the receiver’s balance. This addition is not protected by any overflow check. When the receiver’s balance is already type(uint256).max, adding even a value of 1 causes an overflow and wraps the balance back to 0, silently corrupting state.

// Root cause in the codebase with @> marks to highlight the relevant section
function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
...
let toAmount := sload(toSlot)
...
@> sstore(toSlot, add(toAmount, value)) // unchecked overflow
success := 1
...
}
}

Risk

Likelihood:

  • This occurs whenever a transfer is made to an address whose balance is already close to or equal to type(uint256).max, which can be reached through unrestricted minting or prior overflows.

  • Any system that allows minting large values or interacts with this token without upper-bound checks can unintentionally trigger this condition.

Impact:

  • Receiver balances can overflow and wrap to zero or a low value, effectively destroying tokens and violating the invariant that transfers conserve total value.

  • Accounting corruption may cascade into other logic that relies on balances (governance, staking, collateralization), leading to denial of service or economic loss.

Proof of Concept

The following test shows that transferring tokens to an address with a maximum balance does not revert. Instead, the receiver’s balance silently overflows and wraps to zero.

Explanation:
The receiver is first minted type(uint256).max tokens. The sender is minted a single token and transfers it to the receiver. Inside _transfer, the receiver’s balance update uses add(max, 1), which wraps to 0 in unchecked assembly arithmetic. The transfer succeeds, but the resulting balance is invalid.

function test_transfer_receiverBalanceOverflow() public {
address from = makeAddr("from");
address to = makeAddr("to");
// Set receiver's balance to max
token.mint(to, type(uint256).max);
// Give sender funds
token.mint(from, 1);
vm.prank(from);
token.transfer(to, 1);
// Overflow wraps receiver balance to zero
assertEq(token.balanceOf(to), 0);
}

Recommended Mitigation

Add an explicit overflow check before increasing the receiver’s balance, or move arithmetic out of assembly to rely on Solidity’s checked arithmetic.

_transfer function:

function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
...
let toAmount := sload(toSlot)
+ let newToAmount := add(toAmount, value)
+ if lt(newToAmount, toAmount) {
+ revert(0, 0)
+ }
- sstore(toSlot, add(toAmount, value))
+ sstore(toSlot, newToAmount)
...
}
}

This ensures that transfers to accounts at or near the maximum balance revert instead of corrupting state.

Support

FAQs

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

Give us feedback!