Token-0x

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

Integer Overflow in  _transfer() Allows Token Destruction

Author Revealed upon completion

Integer Overflow in _transfer() Allows Token Destruction

Description

  • The _transfer() function in the ERC20Internals.sol token implementation contains an unchecked arithmetic operation when updating the receiver's balance on line 39.

  • Missing overflow check on receiver balance leads to integer overflow when the receiver has a high token balance, results in token destruction.

// @audit-High Integer overflow, no check of overflow on receiver balance
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)
}
// no total amount check added for receiver
@> sstore(fromSlot, sub(fromAmount, value)) // No overflow check here ..
@> sstore(toSlot, add(toAmount, value)) // Overflow happens here ..
success := 1
mstore(ptr, value)
log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
}
}

Risk

Likelihood:

  • Any user can trigger this accidentally or intentionally

  • No Privileges required

  • Attack requires only a single transfer() call , no attack contract required.

Impact:

  • financial loss for token holders

  • **any **transfer to an address with high balance can trigger overflow

Proof of Concept

Verify the below test using foundry. The test confirms that Bob's balance **decreases **after receiving tokens, proving that overflow occurred and tokens were destroyed.

contract TransferOverflowTest is Test {
Token private token;
address private alice;
address private bob;
function setUp() public {
token = new Token();
alice = makeAddr("alice");
bob = makeAddr("bob");
}
function testOverflowOnTransfer() public {
token.mint(alice, 1000 ether);
uint256 bobInitialBalance = type(uint256).max - 1000 ether + 1;
bytes32 balanceSlot = keccak256(abi.encode(bob, uint256(0)));
vm.store(address(token), balanceSlot, bytes32(bobInitialBalance));
vm.prank(alice);
token.transfer(bob, 1000 ether);
uint256 aliceAfter = token.balanceOf(alice);
uint256 bobAfter = token.balanceOf(bob);
if (bobAfter < bobInitialBalance) {
console.log(" Overflow occurred");
} else {
console.log(" No overflow detected");
}
}
}
TransferOverflowTest::testOverflowOnTransfer()
├─ [45004] Token::mint(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [0] VM::store(Token: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 0x8dadbabab7d896e1d6e41f8f4b514a810d0074773f74b8ece0bf1f294acff3ad, 0xffffffffffffffffffffffffffffffffffffffffffffffc9ca36523a21600000)
│ └─ ← [Return]
├─ [0] VM::prank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Return]
├─ [3393] Token::transfer(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 1000000000000000000000 [1e21])
│ ├─ emit Transfer(from: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], to: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], value: 1000000000000000000000 [1e21])
│ └─ ← [Return] true
├─ [720] Token::balanceOf(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
│ └─ ← [Return] 0
├─ [720] Token::balanceOf(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log(" Overflow occurred") [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]

Recommended Mitigation

  • Add Custom Error: error BalanceOverflow(); // selector: 0x89560ca1

  • Add Overflow check before updating receiver balance.

##### POST Fix when you run testOverFlowOnTransfer ##########
[61982] TransferOverflowTest::testOverflowOnTransfer()
├─ [45004] Token::mint(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [0] VM::store(Token: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 0x8dadbabab7d896e1d6e41f8f4b514a810d0074773f74b8ece0bf1f294acff3ad, 0xffffffffffffffffffffffffffffffffffffffffffffffc9ca36523a21600000)
│ └─ ← [Return]
├─ [0] VM::prank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ └─ ← [Return]
├─ [1201] Token::transfer(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 1000000000000000000000 [1e21])
│ └─ ← [Revert] custom error 0x89560ca1
└─ ← [Revert] custom error 0x89560ca1
function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
+ let newToAmount := add(toAmount, value)
+ if lt(newToAmount, toAmount) {
+ mstore(0x00, shl(224, 0x89560ca1)) // error for balanceoverflow
+ revert(0x00, 0x04)
+ }
sstore(fromSlot, sub(fromAmount, value))
- store(toSlot, add(toAmount, value))
+ sstore(toSlot, newToAmount)
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!