Token-0x

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

Missing balance check in the `ERC20Internals::_burn` function, can cause the user get infinite number of tokens.

Author Revealed upon completion

Root + Impact

Description

  • When executing the ERC20Internals::_burn function, the user can set the value of the value parameter to a number greater than their own balance. Since there is no check for the availability of such number of tokens in the user's account, it can cause underflow and the user's balance increases by a huge number rather than decrease by the value number.


  • This way, the invariant of _totalSupply = Sum of all accounts, will also get broken if _totalSupply is greater than value.


  • In addition, if the value is greater than _totalSupply, it will also underflow and wrap around.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
let supplySlot := _totalSupply.slot
let supply := sload(supplySlot)
// The balance check must happen before changing the `_totalSupply` and `_balances` mappings (state variables).
@> sstore(supplySlot, sub(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
@> sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Risk

Likelihood: High

  • It occurs when the user simply calls the _burn function and tries to burn more tokens than they actually have in their balance.

  • Since the balance is not ckecked, even a user with absolutely no token (balances[user] == 0) can call the function and indirectly mint infinite number of tokens to themself.

Impact: High

  • It has the effect of minting unlimited amount of tokens to the user. This bug makes the token hugely abundant and worthless.

  • It may also severely break the invariant of _totalSupply = Sum of all accounts if the value is only greater than the user's balance but not greater than the _totalSupply.

Proof of Concept

Please copy this function to the Token.t.sol test file and run it.

function test_burnWithInvalidValueMintsUnlimitedTokens() public {
// Arrange
address user1 = makeAddr("user1");
token.mint(user1, 100e18);
address user2 = makeAddr("user2");
uint256 user2BalanceBefore = token.balanceOf(user2);
// Act
token.burn(user2, 1);
uint256 user2BalanceAfter = token.balanceOf(user2);
// Assert
assertEq(user2BalanceBefore, 0, "Initial balance should be zero");
assertEq(user2BalanceAfter, type(uint256).max, "Balance should be max uint256 after burning 1 from zero balance");
assertGt(user2BalanceAfter, token.totalSupply(), "Balance should exceed total supply");
}

Recommended Mitigation

You should make the following changes to resolve the issue.

This code adds the fuctionality for account balance check and moves the state changes to after the balance check section.

function _burn(address account, uint256 value) internal {
assembly ("memory-safe") {
if iszero(account) {
mstore(0x00, shl(224, 0x96c6fd1e))
mstore(add(0x00, 4), 0x00)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let balanceSlot := _balances.slot
- let supplySlot := _totalSupply.slot
- let supply := sload(supplySlot)
- sstore(supplySlot, sub(supply, value))
mstore(ptr, account)
mstore(add(ptr, 0x20), balanceSlot)
let accountBalanceSlot := keccak256(ptr, 0x40)
let accountBalance := sload(accountBalanceSlot)
+ if lt(accountBalance, value) {
+ mstore(0x00, shl(224, 0xe450d38c))
+ mstore(0x04, account)
+ mstore(0x24, accountBalance)
+ mstore(0x44, value)
+ revert(0x00, 0x64)
+ }
+ let supplySlot := _totalSupply.slot
+ let supply := sload(supplySlot)
+ sstore(supplySlot, sub(supply, value))
sstore(accountBalanceSlot, sub(accountBalance, value))
}
}

Support

FAQs

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

Give us feedback!