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.
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;
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:
Impact:
Proof of Concept
Add test_transferOverflow to the Token.T.sol test file
Run with command forge test --match-test test_transferOverflow -vv
function test_transferOverflow() public {
address sender = makeAddr("sender");
address recipient = makeAddr("recipient");
uint256 maxUint = type(uint256).max;
uint256 recipientInitialBalance = maxUint - 100;
uint256 transferAmount = 200;
token.mint(sender, transferAmount);
bytes32 baseSlot = bytes32(uint256(0));
bytes32 recipientBalanceSlot = keccak256(abi.encode(recipient, baseSlot));
vm.store(address(token), recipientBalanceSlot, bytes32(recipientInitialBalance));
uint256 balanceBefore = token.balanceOf(recipient);
assertEq(balanceBefore, recipientInitialBalance, "Recipient should have near-max balance");
vm.prank(sender);
token.transfer(recipient, transferAmount);
uint256 balanceAfter = token.balanceOf(recipient);
uint256 expectedWrappedBalance = 99;
assertEq(balanceAfter, expectedWrappedBalance, "Balance overflowed and wrapped around");
assertLt(balanceAfter, recipientInitialBalance, "Balance decreased due to overflow wrap");
}
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...
}