Token-0x

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

Transfer function does not track recepients balance and can lead to overflow

Author Revealed upon completion

Root + Impact

Description

  • The unchecked addition in _transfer introduces a token balance overflow vulnerability. A user with a sufficiently high balance can make their balance wrap to a lower value and reuse the same balance repeatedly, potentially allowing complete theft of tokens or collapse of token accounting.


// Root cause in the codebase with @> marks to highlight the relevant section
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)
//Only checks senders balance here
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)
}
}

Impact:

  • If a balance overflow occurs, the affected account’s stored balance becomes much lower than its “real” economic balance. This enables several dangerous scenarios depending on how the rest of the system relies on token balances

Proof of Concept

There are many ways to overflow it,especially with this contract,The mint and burn promote overflow and underflow,and can be used to bring any users account to 0.

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(uint).max);
vm.prank(account);
token.transfer(receiver, 1);
balanceSender = token.balanceOf(account);
uint256 balanceReceiver = token.balanceOf(receiver);
assertEq(balanceReceiver, 0);
}

Recommended Mitigation

Add a check for The recievers balance also

+ add this code
// fromSlot update (unchanged)
sstore(fromSlot, sub(fromAmount, value))
// toSlot update with overflow guard
let newToAmount := add(toAmount, value)
if lt(newToAmount, toAmount) {
// overflow happened
// You can use any selector you prefer
mstore(0x00, shl(224, 0x8b3e16f1)) // Overflow(uint256,address)
revert(0x00, 0x04)
}

Support

FAQs

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

Give us feedback!