Complete Arbitrage Tutorial Guide (Expanded)
This expanded tutorial builds upon the walkthrough of setting up and testing arbitrage on Sepolia
testnet. Here we provide a more detailed, workshop-style guide including worked examples for flash
swaps, atomic multi-swap contracts, and considerations for real mainnet arbitrage. We cover: 1.
Environment setup (Foundry, Cast, RPC keys, accounts). 2. Deploying and interacting with ERC20
tokens (USDC, WETH). 3. Adding liquidity on Uniswap V2 pairs. 4. Performing test swaps and
checking balances/reserves. 5. Detecting arbitrage opportunities with getAmountsOut. 6. Executing
arbitrage manually across two routers. 7. Writing a simple atomic contract to bundle two swaps. 8.
Implementing a UniswapV2 flash-swap contract with worked repayment math. 9. Mainnet
considerations: gas, slippage, MEV, sandwiching, liquidity depth, security. Each section includes
code snippets, terminal commands, and example outputs.
Flash Swap Worked Example
A flash swap allows you to borrow tokens from a Uniswap V2 pool, use them within the same
transaction, and repay them plus fees by the end of the transaction. If repayment fails, the entire
transaction reverts. Below is a simplified UniswapV2 flash swap contract. It borrows WETH, swaps
to USDC, then swaps back to WETH on another router. If profit > fee, you keep the difference.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@uniswap/v2-core/contracts/interfaces/[Link]";
import "@uniswap/v2-periphery/contracts/interfaces/[Link]";
import "@openzeppelin/contracts/token/ERC20/[Link]";
contract FlashArb {
address public owner;
IUniswapV2Router02 public router1;
IUniswapV2Router02 public router2;
address public WETH;
address public USDC;
constructor(address _router1, address _router2, address _WETH, address _USDC) {
owner = [Link];
router1 = IUniswapV2Router02(_router1);
router2 = IUniswapV2Router02(_router2);
WETH = _WETH;
USDC = _USDC;
}
// Initiate flash swap from Uniswap pair
function startArb(address pair, uint amountWETH) external {
IUniswapV2Pair(pair).swap(amountWETH, 0, address(this), bytes("flash"));
}
// This function is called back by the UniswapV2Pair after swap()
function uniswapV2Call(address, uint amount0, uint, bytes calldata) external {
uint amountWETH = amount0;
// Swap WETH -> USDC on router2
IERC20(WETH).approve(address(router2), amountWETH);
address[] memory path = new address[](2);
path[0] = WETH;
path[1] = USDC;
uint[] memory usdcOut = [Link](
amountWETH, 0, path, address(this), [Link]
);
// Swap USDC -> WETH on router1
IERC20(USDC).approve(address(router1), usdcOut[1]);
address[] memory path2 = new address[](2);
path2[0] = USDC;
path2[1] = WETH;
uint[] memory wethOut = [Link](
usdcOut[1], 0, path2, address(this), [Link]
);
// Compute repayment + fee
uint fee = (amountWETH * 3) / 997 + 1; // 0.3% fee approx
uint repayment = amountWETH + fee;
require(wethOut[1] > repayment, "No profit");
// Repay flash loan
IERC20(WETH).transfer([Link], repayment);
// Profit left in contract
}
function withdraw() external {
require([Link] == owner, "Not owner");
IERC20(WETH).transfer(owner, IERC20(WETH).balanceOf(address(this)));
IERC20(USDC).transfer(owner, IERC20(USDC).balanceOf(address(this)));
}
}
Repayment Math Example: Suppose you borrow 1 WETH in a flash swap. The Uniswap fee is
0.3%, so repayment = 1 * (1000/997) ≈ 1.003 WETH. If your two-hop swaps return 1.01 WETH, you
make 0.007 WETH profit. If they return ≤ 1.003, the transaction reverts. Things to Consider for
Mainnet Arbitrage: - Gas costs: must be lower than profit, otherwise you lose money. - Slippage:
liquidity depth matters. Large trades move price. - MEV risk: bots may frontrun or backrun your
transaction. - Security: use audited libraries, avoid reentrancy. - Execution speed: bundle swaps
atomically, avoid holding tokens manually.