Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

“Allowance race-condition / ‘multiple withdrawal’ attack possible due to standard approve semantics”

Root + Impact

Description

  • The token uses the canonical approve(spender, amount) interface per ERC20. As with many ERC20s, this allows a classic “multiple withdrawal” / double-spend risk: when a user changes an allowance from N → M (non-zero to non-zero), a malicious spender can front-run the new allowance and withdraw both N + M, draining more than intended. This is a known flaw in the ERC20 standard.

    Alice approves Spender for 100.
    Alice later wants to reduce allowance to 50: calls approve(spender, 50).
    Spender sees the pending tx, front-runs to call transferFrom for 100, then after Alice’s tx executes, allowance becomes 50. Spender can then immediately transfer another 50 → total 150.
    //implementation probably looks like:
    function approve(address spender, uint256 amount) public returns (bool) {
    _allowances[msg.sender][spender] = amount;
    emit Approval(msg.sender, spender, amount);
    return true;
    }

Risk

Likelihood:

  • Moderate — occurs any time a user or contract tries to change a non-zero allowance to another non-zero value. This is common in many applications.

  • The issue stems from standard ERC20 behavior (not a bug), and many ERC20 tokens accept this trade-off. But given that Token-0x is meant to be a “base token” for protocols, the risk to user funds remains real. With proper frontend guidance or usage of “approve-to-zero-then-set” pattern, risk can be significantly reduced.

Impact:

  • Funds can be stolen: spender drains more tokens than user intended.

Proof of Concept

  1. Alice approves spender for 100

Alice wants to change it to 50 → calls approve(spender, 50)
3. Spender front-runs and:

  • first withdraws the existing allowance (100)

  • then withdraws the new allowance (50)

contract Spender {
function attack(address token, address victim) external {
// Step 1: Front-run → drain old allowance (100)
IERC20(token).transferFrom(victim, msg.sender, 100 ether);
// Step 2: After victim tx sets allowance to 50 → drain again
IERC20(token).transferFrom(victim, msg.sender, 50 ether);
}
}
contract TestRaceCondition {
Token0x token;
Spender attacker;
function testRace() external {
token = new Token0x();
attacker = new Spender();
// Give Alice tokens
token.mint(address(this), 200 ether);
// Approve attacker for 100
token.approve(address(attacker), 100 ether);
// Victim tries to change allowance to 50
// attacker front-runs this tx
attacker.attack(address(token), address(this));
// PoC: attacker drains 150 instead of 50
require(token.balanceOf(address(attacker)) == 150 ether, "Attacker stole 150 ether");
}
}

Recommended Mitigation

  • Encourage use of “approve(0)” then “approve(newAmount)” when changing allowances.

Provide helper functions: increaseAllowance / decreaseAllowance instead of direct approve.


- remove this code
+ add this code
- function approve(address spender, uint256 amount) public returns (bool) {
- _allowances[msg.sender][spender] = amount;
- emit Approval(msg.sender, spender, amount);
- return true;
- }
+ function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
+ uint256 old = _allowances[msg.sender][spender];
+ uint256 newVal = old + addedValue;
+ _allowances[msg.sender][spender] = newVal;
+ emit Approval(msg.sender, spender, newVal);
+ return true;
+ }
+
+ function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
+ uint256 old = _allowances[msg.sender][spender];
+ require(old >= subtractedValue, "ERC20: decreased allowance below zero");
+ uint256 newVal = old - subtractedValue;
+ _allowances[msg.sender][spender] = newVal;
+ emit Approval(msg.sender, spender, newVal);
+ return true;
+ }
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Known issue

Appeal created

pushprakash23 Submitter
17 days ago
gaurangbrdv Lead Judge
17 days ago
gaurangbrdv Lead Judge
17 days ago
gaurangbrdv Lead Judge 14 days ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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

Give us feedback!