Root + Impact
Description
-
A transfer should:
-
Issues
The function performs balance updates using the raw EVM opcodes sub and add inside an assembly block.
These arithmetic operations do not include overflow or underflow checks.
Although the insufficient-balance check prevents underflow for the sender, there is no protection against overflow on the receiver’s balance
Root Cause
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:
Many real-world flows involve large cumulative transfers (bridges, batch transfers, staking payouts), increasing exposure.
Impact:
_mint functionality (if present) can be used to inflate balances to near-overflow values.
Proof of Concept
In the test we can clearly see that the balance of the receiver was type(unit256).max
When even 2 tokens were transferred the call was successful and receiver's balance turned to 1, instead the call should be reverted
function test_transfer_overflow() public {
address account = makeAddr("account");
token.mint(account, 100e18);
uint256 balanceSender = token.balanceOf(account);
assertEq(balanceSender, 100e18);
address receiver = makeAddr("receiver");
token.mint(receiver, type(uint256).max);
vm.prank(account);
token.transfer(receiver, 2);
console.log(token.balanceOf(receiver));
}
Ran 1 test for test/Token.t.sol:TokenTest
[PASS] test_transfer_overflow() (gas: 92632)
Logs:
1
Traces:
[92632] TokenTest::test_transfer_overflow()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e]
├─ [0] VM::label(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e], "account")
│ └─ ← [Return]
├─ [45004] Token::mint(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e], 100000000000000000000 [1e20])
│ └─ ← [Stop]
├─ [720] Token::balanceOf(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e]) [staticcall]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] receiver: [0xB6D4805bf6943c5875C0C7b67EDa24b2bDACBF6e]
├─ [0] VM::label(receiver: [0xB6D4805bf6943c5875C0C7b67EDa24b2bDACBF6e], "receiver")
│ └─ ← [Return]
├─ [23104] Token::mint(receiver: [0xB6D4805bf6943c5875C0C7b67EDa24b2bDACBF6e], 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
│ └─ ← [Stop]
├─ [0] VM::prank(account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e])
│ └─ ← [Return]
├─ [3393] Token::transfer(receiver: [0xB6D4805bf6943c5875C0C7b67EDa24b2bDACBF6e], 2)
│ ├─ emit Transfer(from: account: [0x359e534d4C79745c3c0A0BC80d80cfAe9e82699e], to: receiver: [0xB6D4805bf6943c5875C0C7b67EDa24b2bDACBF6e], value: 2)
│ └─ ← [Return] true
├─ [720] Token::balanceOf(receiver: [0xB6D4805bf6943c5875C0C7b67EDa24b2bDACBF6e]) [staticcall]
│ └─ ← [Return] 1
├─ [0] console::log(1) [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]
Recommended Mitigation
Adds proper zero-address and insufficient-balance validations aligned with ERC-20 standards.
function _transfer(address from, address to, uint256 value) internal {
require(from != address(0), "ERC20: transfer from zero");
require(to != address(0), "ERC20: transfer to zero");
uint256 fromBal = _balances[from];
require(fromBal >= value, "ERC20: insufficient balance");
_balances[from] = fromBal - value;
_balances[to] += value;
emit Transfer(from, to, value);
}