Root + Impact
Description
-
Normally, _transfer should subtract value from the sender and add value to the receiver, reverting if either operation would exceed uint256 bounds so balances remain consistent.
-
But In this implementation, the receiver-side addition happens inside assembly without an overflow check, so once a receiver’s balance is near uint256 max, receiving one more token wraps their balance back to zero.
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))
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}
Risk
Likelihood:
-
Reason 1 : Anytime _transfer is reachable (standard ERC-20 transfers), the overflow triggers the moment a saturated receiver accepts another token
-
Reason 2: Attackers only need to prefill the receiver to type(uint256).max via existing mint/burn flows, so they can force the condition whenever those are exposed.
Impact:
-
Impact 1: Receiver balances silently wrap to zero while total supply stays high, breaking accounting and enabling theft or griefing.
-
Impact 2: Downstream protocols relying on balances (DEXes, lending....) can be drained or bricked because the victim loses tokens without compensation.
Proof of Concept
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
contract TransferHarness is ERC20 {
constructor() ERC20("Token", "TKN") {}
function exposedMint(address account, uint256 value) external {
_mint(account, value);
}
}
contract TransferOverflow is Test {
TransferHarness internal token;
address internal sender = address(0x2030);
address internal receiver = address(0x2031);
function setUp() public {
token = new TransferHarness();
}
function test_transferOverflow() public {
token.exposedMint(receiver, type(uint256).max);
token.exposedMint(sender, 2);
vm.prank(sender);
token.transfer(receiver, 1);
assertEq(token.balanceOf(receiver), 0, "receiver balance wrapped to zero");
assertEq(token.balanceOf(sender), 1, "sender balance decremented but stayed positive");
}
}
Recommended Mitigation
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)
+ let toHeadroom := sub(not(0), toAmount)
+ if gt(value, toHeadroom) {
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(add(0x00, 4), to)
+ mstore(add(0x00, 0x24), toAmount)
+ mstore(add(0x00, 0x44), value)
+ revert(0x00, 0x64)
+ }
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))
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}