Token-0x

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

Approve Function Allows Front-Running Attack Enabling Double Spending of Allowances

Author Revealed upon completion

[M-1] Approve Function Allows Front-Running Attack Enabling Double Spending of Allowances

Description

  • The approve() function allows changing allowances directly, which can be front-run by the spender to extract both old and new allowance amounts.

function _approve(address owner, address spender, uint256 value) internal virtual returns (bool success) { // approve function to set allowance
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) // overwrites the value, No check!
//missing current allowance check
success := 1
mstore(0x00, value)
log3(0x00, 0x20, 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, owner, spender)
}
}

Risk

Likelihood:

  • User approves Spender for 5000 tokens

  • User later changes approval to 2500 tokens

  • Spender front-runs with transferFrom(5000) - (can watch the pending transactions and issue a transferFrom transaction to move the originally approved 5000 tokens)

  • User's approve(2500) executes

  • Spender can now spend another 2500

  • Total stolen: 7500 instead of 2500


Impact:

  • The approve function does not protect against the double-spending problem.

  • leads to front running opportunity

Proof of Concept

  • run the below test using forge that proves the code is vulnerable to front runnig attack:.

contract ERC20TokenTest is Test {
Token private token;
address private constant owner = address(0x3);
address private constant spender = address(0x4);
function setUp() public {
token = new Token();
token.mint(owner, 5000);
}
function testFrontrunExploit() public {
uint256 initialAllowance = 1000;
uint256 newAllowance = 500;
vm.prank(owner);
token.approve(spender, initialAllowance);
// Attacker exploits by frontrunning
vm.prank(spender);
token.transferFrom(owner, spender, initialAllowance); // Spend old allowance
vm.prank(owner);
token.approve(spender, newAllowance); // setting New allowance
vm.prank(spender);
token.transferFrom(owner, spender, newAllowance); // Spend new allowance too
uint256 totalStolen = token.balanceOf(spender);
console.log("Total tokens stolen by spender:", totalStolen);
console.log("Total balance left with owner:", token.balanceOf(owner));
}
}

Recommended Mitigation

To prevent front-running vulnerabilities in ERC20 tokens, using the safeApprove, increaseAllowance and decreaseAllowance functions stands out as particularly effective. This approach addresses the core issue of the traditional approve function, which can expose users to front-running attacks during the allowance adjustment period.

+ function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
+ ....statement
+}
+function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
+ .... statement
+}
+function safeApprove(address spender, uint256 currentValue, uint256 newValue) public returns (bool) {
+ .... StatemenT
+}

Support

FAQs

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

Give us feedback!