
AuditSolidityWeb3 Security
How to Prevent Front-Running in ERC20 Smart Contracts
21 de mayo de 2024•Carlos (Bloqarl)
Learn advanced techniques and secure smart contract practices to protect your ERC20 tokens from front-running, exploits, and unauthorized access.
What This Guide Covers on ERC20 Front-Running Attacks
This article explores the critical issue of front-running vulnerabilities in ERC20 tokens on the Ethereum blockchain, focusing on how these security gaps can be exploited through the token allowance mechanism.
We provide a detailed examination of the vulnerability, illustrate it using a specific contract example, and showcase a proof-of-concept attack to highlight the risks.
Read on to discover how to safeguard your digital assets with enhanced security measures tailored for the ERC20 standard.
Introduction
Front-running attacks exploit the fact that transactions on the Ethereum network are processed in the order determined by the miners, based on the gas price offered. This vulnerability can lead to situations where an attacker can front-run a legitimate transaction by offering a higher gas price, thereby manipulating the state of the contract before the original transaction is processed.
One common instance of this vulnerability is the ERC20 token standard's
approve
function, which allows a user to approve another address to spend a certain amount of tokens on their behalf. If not implemented correctly, an attacker can front-run the approval transaction and drain the user's tokens.How ERC20 approve
Vulnerabilities Enable Front-Running
1. How Front-Running Works on Ethereum and ERC20 Tokens
Definition:
Front running in cryptocurrencies typically occurs when someone with knowledge of an upcoming transaction (from the public mempool) uses this information to their advantage by placing a transaction in such a way that it is confirmed before the known transaction.
Front running in cryptocurrencies typically occurs when someone with knowledge of an upcoming transaction (from the public mempool) uses this information to their advantage by placing a transaction in such a way that it is confirmed before the known transaction.
ERC20 Context:
For ERC20 tokens, front running often involves seeing an
For ERC20 tokens, front running often involves seeing an
approve
transaction pending and quickly using the current allowance before the new approve
transaction that modifies this allowance is processed.2. Example of a Vulnerable ERC20 approve
Contract
Here is a simplified example of an existing vulnerable contract. The complete implementation you can find it in Etherscan.
1pragma solidity 0.6.4;2//ERC20 Interface3interface ERC20 {4 [...]5}6interface VETH {7 [...]8}9library SafeMath {10 [...]11}12 //======================================VETHER=========================================//13contract Vether4 is ERC20 {14 using SafeMath for uint;15 // ERC-20 Parameters16 string public name;17 string public symbol;18 uint public decimals;19 uint public override totalSupply;20 // ERC-20 Mappings21 mapping(address => uint) private _balances;22 mapping(address => mapping(address => uint)) private _allowances;23 // Rest of Parameters24 [...]25 //=====================================CREATION=========================================//26 // Constructor27 constructor() public {28 [...]29 }30 function _setMappings() internal {31 [...]32 }3334 //========================================ERC20=========================================//35 function balanceOf(address account) public view override returns (uint256) {36 return _balances[account];37 }38 function allowance(address owner, address spender) public view virtual override returns (uint256) {39 return _allowances[owner][spender];40 }41 // ERC20 Transfer function42 function transfer(address to, uint value) public override returns (bool success) {43 _transfer(msg.sender, to, value);44 return true;45 }46 // ERC20 Approve function47 function approve(address spender, uint value) public override returns (bool success) {48 _allowances[msg.sender][spender] = value;49 emit Approval(msg.sender, spender, value);50 return true;51 }52 // ERC20 TransferFrom function53 function transferFrom(address from, address to, uint value) public override returns (bool success) {54 require(value <= _allowances[from][msg.sender], 'Must not send more than allowance');55 _allowances[from][msg.sender] = _allowances[from][msg.sender].sub(value);56 _transfer(from, to, value);57 return true;58 }59 // Internal transfer function which includes the Fee60 function _transfer(address _from, address _to, uint _value) private {61 require(_balances[_from] >= _value, 'Must not send more than balance');62 require(_balances[_to] + _value >= _balances[_to], 'Balance overflow');63 _balances[_from] =_balances[_from].sub(_value);64 uint _fee = _getFee(_from, _to, _value); // Get fee amount65 _balances[_to] += (_value.sub(_fee)); // Add to receiver66 _balances[address(this)] += _fee; // Add fee to self67 totalFees += _fee; // Track fees collected68 emit Transfer(_from, _to, (_value.sub(_fee))); // Transfer event69 if (!mapAddress_Excluded[_from] && !mapAddress_Excluded[_to]) {70 emit Transfer(_from, address(this), _fee); // Fee Transfer event71 }72 }73 // Calculate Fee amount74 function _getFee(address _from, address _to, uint _value) private view returns (uint) {75 if (mapAddress_Excluded[_from] || mapAddress_Excluded[_to]) {76 return 0; // No fee if excluded77 } else {78 return (_value / 1000); // Fee amount = 0.1%79 }80 }8182 //================================REST OF THE CONTRACT======================================//83 [...]84}
Specific Vulnerability Points in Vether4
approve
and transferFrom
Mechanism:
Scenario:
Suppose Alice decides to change an approval for Bob from 5000 VETH to 2500 VETH. She submits an
Suppose Alice decides to change an approval for Bob from 5000 VETH to 2500 VETH. She submits an
approve
transaction setting Bob's allowance to 2500 VETH.Front-Running Opportunity:
A malicious actor (or Bob himself) can watch the pending transactions and issue a
A malicious actor (or Bob himself) can watch the pending transactions and issue a
transferFrom
transaction to move the originally approved 5000 VETH before Alice's new approve
transaction is confirmed. If the front-runner’s transaction is mined first, they can exploit the old allowance fully before it’s reduced.Lack of Checks in approve
Function:
The
approve
function directly sets the allowance of a spender without considering any previously set allowances. This can lead to a situation known as a "race condition" where swiftly executed transactions can manipulate allowances to transfer more than intended by the token owner.No Protection Against Double-Spending Allowance:
The
approve
function does not protect against the double-spending problem. If a user lowers the allowance of a spender who already has a certain allowance, the spender can quickly spend the existing allowance and still access the newly set allowance if the approve
transaction is mined afterward.3. Foundry PoC: Simulating the Front-Running Attack
- Alice approves an allowance of 5000 VETH to Bob.
- Alice attempts to lower the allowance to 2500 VETH.
- Bob notices the transaction in the mempool and front-runs it by using up the full allowance with a
transferFrom
call. - Alice's lowered allowance is confirmed and Bob now has an allowance of 2500 VETH, which can be spent further for a total of 7500 VETH.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.0;34import {Test, console} from "forge-std/Test.sol";56interface IVether4 {7 function transfer(address to, uint amount) external returns (bool);8 function balanceOf(address account) external view returns (uint);9 function approve(address spender, uint amount) external returns (bool);10 function transferFrom(address from, address to, uint amount) external returns (bool);11}1213contract AllowanceRaceConditionTest is Test {14 IVether4 vether;1516 address constant vetherAddress = 0x4Ba6dDd7b89ed838FEd25d208D4f644106E34279; // Mainnet address of Vether41718 address alice = 0x273BA31C706b9D9FdAe1FD999183Bfa865895bE9;19 address bob = 0x159e4E57eD13176A69693c987917651C552d2575;2021 function setUp() public {22 vether = IVether4(vetherAddress);23 }2425 function testAllowanceRaceCondition() public {26 uint256 bobInitialBalance = vether.balanceOf(bob);27 uint256 aliceInitialBalance = vether.balanceOf(alice);2829 console.log("Bob's initial balance: ", bobInitialBalance);30 console.log("Alice's initial balance: ", aliceInitialBalance);3132 // Alice approves Bob to spend 5000 VETH33 vm.prank(alice);34 vether.approve(bob, 5000 ether); // Using 'ether' to convert to correct token units if necessary3536 // Bob tries to front-run Alice's next approval by transferring 5000 VETH to himself37 vm.prank(bob);38 vether.transferFrom(alice, bob, 5000 ether);3940 // Alice lowers the allowance to 2500 VETH41 vm.prank(alice);42 vether.approve(bob, 2500 ether);4344 // Bob attempts to transfer the newly approved 2500 VETH45 vm.prank(bob);46 bool txSuccess = vether.transferFrom(alice, bob, 2500 ether);4748 uint256 bobFinalBalance = vether.balanceOf(bob);49 uint256 expectedBobBalance = bobInitialBalance + 7500 ether; // Sum of first and second transfer5051 assertEq(bobFinalBalance, expectedBobBalance, "Bob's final balance should reflect the transfers");52 assertTrue(txSuccess, "Bob's second transfer should succeed within allowance");53 }54}
Contract Interface and Setup:
IVether4 Interface:
This defines the expected functions of the Vether4 contract, allowing the test contract to interact with Vether4 as if it were a local Solidity contract.
This defines the expected functions of the Vether4 contract, allowing the test contract to interact with Vether4 as if it were a local Solidity contract.
setUp Function:
This function initializes the Vether4 contract interface by pointing it to the deployed address of Vether4 on mainnet. This setup occurs before each test is run.
This function initializes the Vether4 contract interface by pointing it to the deployed address of Vether4 on mainnet. This setup occurs before each test is run.
Test Functionality (testAllowanceRaceCondition
):
Initial Balance Checks:
The test first logs the initial Ethereum balances of Bob and Alice. It's important to note that if
The test first logs the initial Ethereum balances of Bob and Alice. It's important to note that if
bob.balance
and alice.balance
refer to their ETH balances, they may not reflect changes from Vether token transfers. For token balance checks, vether.balanceOf(address)
should be used instead.Approve and TransferFrom:
The test simulates a common scenario where Alice approves Bob to spend a certain amount of Vether tokens, and then attempts to change this allowance. However, before the new allowance is set, Bob tries to transfer the initially approved amount.
The test simulates a common scenario where Alice approves Bob to spend a certain amount of Vether tokens, and then attempts to change this allowance. However, before the new allowance is set, Bob tries to transfer the initially approved amount.
- Alice first approves Bob to spend 5000 VETH.
- Bob, potentially acting maliciously or taking advantage of the timing, transfers 5000 VETH to himself before Alice can lower the allowance.
- Alice then lowers the allowance to 2500 VETH.
- Bob attempts to transfer again under the new allowance.
Balance Validation:
After the operations, the test checks if Bob's final token balance matches the expected value, considering both transfers. This is crucial to validate that the token transfer operations respect the allowances set and changed over time.
After the operations, the test checks if Bob's final token balance matches the expected value, considering both transfers. This is crucial to validate that the token transfer operations respect the allowances set and changed over time.
Security Implications:
This test effectively demonstrates a race condition where Bob can use the full initial allowance before it's successfully reduced. It showcases the importance of handling allowances carefully, especially in multi-user environments where timing differences can lead to unexpected or exploitative behaviors.
Running the Test with Forge Test Fork
Forge Test Fork:
You are running these tests on a fork of the main Ethereum network. This allows you to interact with the current state of the Ethereum mainnet in a controlled, isolated environment. The
You are running these tests on a fork of the main Ethereum network. This allows you to interact with the current state of the Ethereum mainnet in a controlled, isolated environment. The
--fork-url
parameter points to an Alchemy node that provides access to this forked version of the network.Command to Run:
1forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/your-api-key
How to Prevent ERC20 Front-Running Vulnerabilities
Among various strategies to prevent front-running vulnerabilities in ERC20 tokens, using the
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.Why
increaseAllowance
and decreaseAllowance
Are Effective:Minimizes the Race Condition Window:
These methods reduce the risk of front-running by minimizing the window of time during which the allowances are vulnerable. By adjusting allowances incrementally, it ensures that any front-running transaction cannot exploit a large, suddenly granted allowance.
These methods reduce the risk of front-running by minimizing the window of time during which the allowances are vulnerable. By adjusting allowances incrementally, it ensures that any front-running transaction cannot exploit a large, suddenly granted allowance.
Enhances Control Over Allowance Changes:
By allowing token holders to specify exactly how much to increase or decrease the allowance, these functions offer more precise control over the changes, reducing the likelihood of unintentionally setting incorrect allowances.
By allowing token holders to specify exactly how much to increase or decrease the allowance, these functions offer more precise control over the changes, reducing the likelihood of unintentionally setting incorrect allowances.
Prevents the Zero-Reset Vulnerability:
The traditional method of resetting an allowance to zero before setting a new higher value can be risky if the
The traditional method of resetting an allowance to zero before setting a new higher value can be risky if the
approve
transaction is front-run after the zero reset but before the new value is set. The increaseAllowance
and decreaseAllowance
functions inherently avoid this pitfall by directly adjusting the existing value.1function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {2 _approve(msg.sender, spender, allowance(msg.sender, spender).add(addedValue));3 return true;4}56function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {7 _approve(msg.sender, spender, allowance(msg.sender, spender).sub(subtractedValue, "ERC20: decreased allowance below zero"));8 return true;9}
Conclusion
In conclusion, this exploration of front-running vulnerabilities within the ERC20 token standard has highlighted the crucial need for improved security measures. By detailing how attackers exploit transaction order on the Ethereum network, particularly through the
approve
function, we've emphasized the importance of safeguarding digital assets. Implementing solutions like the safeApprove
method and adopting the ERC20 with Permit standard can significantly reduce risks, making smart contracts more robust against such exploits.For developers and participants in the cryptocurrency space, prioritizing these enhancements is not just about protecting investments; it's about fostering trust and stability in blockchain technologies. As we continue to innovate, the integration of rigorous security practices will be key to sustaining growth and ensuring that blockchain fulfills its promise of secure, decentralized transactions. These improvements are essential for anyone looking to enhance the integrity and functionality of their digital assets on the Ethereum platform.
About Zealynx Security
Front-running vulnerabilities are avoidable, but only if you know where to look. At Zealynx Security, we help teams strengthen smart contracts through expert audits, fuzzing, and targeted tests that catch the kinds of exploits covered here. If you're building with ERC20 or need peace of mind before launch, reach out to us or explore how we can help.
Connect with us:
References: