Token-0x

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

possible race condition on `approve` with overrides update

Author Revealed upon completion

Root + Impact

Description

ERC20::approve allows a token holder to give permission to anyone to spend the token on their behalf. this is normal and follows ERC20 standard. however, the issue is on how the update is done on the approval.

ERC20::approve:

function approve(address spender, uint256 value) public virtual returns (bool success) {
address owner = msg.sender;
success = _approve(owner, spender, value);
}

ERC20Internals::_approve:

function _approve(address owner, address spender, uint256 value) internal virtual returns (bool success) {
assembly ("memory-safe") {
if iszero(owner) {
mstore(0x00, shl(224, 0xe602df05))
mstore(add(0x00, 4), owner)
revert(0x00, 0x24)
}
if iszero(spender) {
mstore(0x00, shl(224, 0x94280d62))
mstore(add(0x00, 4), spender)
revert(0x00, 0x24)
}
let ptr := mload(0x40)
let baseSlot := _allowances.slot
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let initialHash := keccak256(ptr, 0x40)
mstore(ptr, spender)
mstore(add(ptr, 0x20), initialHash)
let allowanceSlot := keccak256(ptr, 0x40)
sstore(allowanceSlot, value)
success := 1
mstore(0x00, value)
log3(0x00, 0x20, 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, owner, spender)
}
}

Risk

Likelihood: Medium/Low

  • it requires the spender to know that the token holder he'll spend on behalf will update the allowance in next transaction

  • it also requires that the token holder is holding at least more than the current allowance amount (before updates)

  • Reason 2

Impact: High

  • the spender can unauthorizedly spend more token than the owner intended amount to spend

Proof of Concept

  1. user1 and user2 has some tokens (e.g. both has 50 tokens each, though user2 having tokens is entirely optional)

  2. user1 being the owner giving permission to user2 to spend 20 tokens on user1 behalf

  3. after the transaction has successfully submitted and executed, user1 realized that the allowance should be 30 tokens, rather than 20 tokens

  4. user1 updates the allowance to 30 tokens

  5. here's the catch, user2 found out user1's mistake on incorrect allowance amount, and saw the transaction submitted, user2 hold the transaction (step 4) and frontrun it by calling transaferFrom to force transfer the available allowance amount to himself

  6. after that, user1 transaction (step 4) starts to get executed

  7. allowance does indeed gets updated from 20 tokens to 30 tokens, but during the updates, user2 has stolen the 20 tokens from user1

function test_race_condition() public {
// prepare users some tokens
token.mint(user1, 50 ether);
token.mint(user2, 50 ether);
assertEq(token.balanceOf(user1), 50 ether);
assertEq(token.balanceOf(user2), 50 ether);
// user1 approve user2 to spend 20 tokens on his behalf
vm.prank(user1);
token.approve(user2, 20 ether);
assertEq(token.allowance(user1, user2), 20 ether);
// user1 found out that he should give permission to spend 30 tokens
// rather than 20 tokens .... user2 found out user1 next move, frontrun
// the allowance !!!
// user1 tx submitted into the pool: token.approve(user2, 30 ether)
// user2 frontrun this tx before it is executed
vm.prank(user2);
token.transferFrom(user1, user2, 20 ether);
// after this, user1 tx start its execution ...
vm.prank(user1);
token.approve(user2, 30 ether);
assertEq(token.allowance(user1, user2), 30 ether);
// confirm the consequences after the frontrun
assertEq(token.balanceOf(user1), 50 ether - 20 ether); // 20 tokens just got stolen during the frontrun
assertEq(token.balanceOf(user2), 50 ether + 20 ether); // 20 tokens stolen to this user
}

Recommended Mitigation

rather than updates the allowance by overriding the existing amount, consider using increase/decrease (e.g. increaseAllowance(), decreaseAllowance())

as such, user1 can update the allowance by calling increaseAllowance(user2, 10 ether) to increase the allowance by 10 tokens.

Support

FAQs

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

Give us feedback!