Token-0x

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

[H-03] Unchecked Arithmetic Overflow in _transfer Function (Missing Overflow Protection in Yul Assembly)

Author Revealed upon completion

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))// <@ Missing overflow check: toAmount + value may exceed max(uint256)
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)
}
}

Support

FAQs

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

Give us feedback!