Solidity MEV Concepts
MEV is extra value taken from the order of trades inside a block. The order of trades inside a block can change what each trade pays.
Order changes the price
Two identical swaps in a row do not pay the same amount. The first swap moves the reserves, so the second swap gets less.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyPool {
uint256 public reserveA;
uint256 public reserveB;
constructor() {
reserveA = 100;
reserveB = 100;
}
function swapAForB(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveB * amountIn / (reserveA + amountIn);
reserveA += amountIn;
reserveB -= amountOut;
return amountOut;
}
function swapBForA(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveA * amountIn / (reserveB + amountIn);
reserveB += amountIn;
reserveA -= amountOut;
return amountOut;
}
}A trade placed first takes the better price
A victim trade alone would get 9 tokens out. After one trade runs first, the same victim trade gets only 7.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyPool {
uint256 public reserveA;
uint256 public reserveB;
constructor() {
reserveA = 100;
reserveB = 100;
}
function swapAForB(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveB * amountIn / (reserveA + amountIn);
reserveA += amountIn;
reserveB -= amountOut;
return amountOut;
}
function swapBForA(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveA * amountIn / (reserveB + amountIn);
reserveB += amountIn;
reserveA -= amountOut;
return amountOut;
}
}A sandwich profits from both moves
The attacker buys before the victim and sells after the victim. The attacker ends with more than it started with.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyPool {
uint256 public reserveA;
uint256 public reserveB;
constructor() {
reserveA = 100;
reserveB = 100;
}
function swapAForB(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveB * amountIn / (reserveA + amountIn);
reserveA += amountIn;
reserveB -= amountOut;
return amountOut;
}
function swapBForA(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveA * amountIn / (reserveB + amountIn);
reserveB += amountIn;
reserveA -= amountOut;
return amountOut;
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ToyPool {
uint256 public reserveA;
uint256 public reserveB;
constructor() {
reserveA = 100;
reserveB = 100;
}
function swapAForB(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveB * amountIn / (reserveA + amountIn);
reserveA += amountIn;
reserveB -= amountOut;
return amountOut;
}
function swapBForA(uint256 amountIn) public returns (uint256) {
uint256 amountOut = reserveA * amountIn / (reserveB + amountIn);
reserveB += amountIn;
reserveA -= amountOut;
return amountOut;
}
}Key points
- MEV: extra value taken from the order of trades inside a block.
- Order: the first trade in a block moves the pool price for the trades after it.
- Front-running: a trade placed before a victim trade takes the better price first.
- Back-running: a trade placed after a victim trade profits from the price move the victim caused.
- Sandwich: a buy before and a sell after a victim trade, profiting from both price moves.
In this example, the pool starts with 100 of each token, the attacker swaps 10 for 9, the victim swaps 10 for only 7, and the attacker swaps its 9 back for 11. The order of the three swaps is the whole story.
Note
this lesson uses a toy pool on a local test chain only, never a real pool or a real price.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A victim swap alone returns 9 tokens. One identical swap runs before it. What does the victim swap now return in the toy pool?