Description
The _transfer function performs an addition operation in Yul assembly without overflow check when updating the recipient's balance. Since Solidity 0.8.x's automatic overflow protection does not apply to inline assembly, an attacker can transfer tokens that cause integer overflow, corrupting the recipient's balance.
Root Cause
The function uses Yul assembly for gas optimization but omits mandatory overflow checks for addition operations. The vulnerability occurs because the add opcode in Yul silently wraps on overflow, unlike Solidity's checked arithmetic
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:
-
Any user can trigger this overflow by transferring tokens to an account that already has a balance close to type(uint256).max.
-
Easy Exploitation: The attack requires only a standard transfer transaction to a targeted account with a calculated amount.
Impact:
-
Direct Fund Loss: The recipient's balance can be reduced from near-maximum to near-zero through overflow corruption.
-
Protocol Disruption: The token's economic model is compromised, as balances can be artificially manipulated.
-
Balance Corruption: Individual account balances become mathematically incorrect, breaking token accounting.
Proof of Concept
function test_transfer_overflow_addition() public {
uint256 max = type(uint256).max;
uint256 almostMax = max - 1000;
token.mint(bob, almostMax);
token.mint(alice, 2000);
uint256 bobBalanceBefore = token.balanceOf(bob);
uint256 aliceBalanceBefore = token.balanceOf(alice);
console.log("Balance Bob avant transfert:", bobBalanceBefore);
console.log("Balance Alice avant transfert:", aliceBalanceBefore);
vm.startPrank(alice);
bool success = token.transfer(bob,2000);
vm.stopPrank();
assertTrue(success, "Transfert Sucees don't revert with potential overflow");
uint256 bobBalanceAfter = token.balanceOf(bob);
uint256 aliceBalanceAfter = token.balanceOf(alice);
console.log("Balance Bob apres transfert:", bobBalanceAfter);
console.log("Balance Alice apres transfert:", aliceBalanceAfter);
uint256 expectedBobBalance = 999;
assertEq(bobBalanceAfter, expectedBobBalance, "Overflow in transfer non verifi");
assertEq(aliceBalanceAfter, 0, "Alice Balance incorrect");
}
Result :
[PASS] test_transfer_overflow_addition() (gas: 80867)
Logs:
Bob Balance before transfert: 115792089237316195423570985008687907853269984665640564039457584007913129638935
Alice Balance before transfert: 2000
Bob Balance after transfert: 999
Alice Balance after transfert: 0
Recommended Mitigation
Add explicit overflow check before the addition operation in the _transfer function
Alternative Solution: Create reusable safe arithmetic functions (e.g., safeAdd and safeSub) to avoid code duplication and ensure consistent overflow protection across the entire contract.
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))
+ if gt(value, sub(not(0), toAmount)) {
+ // Panic: arithmetic overflow (error code 0x11)
+ mstore(0x00, shl(224, 0x4e487b71))
+ mstore(0x04, 0x11)
+ revert(0x00, 0x24)
+ }
sstore(toSlot, add(toAmount, value))
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}