Comment on page
Understanding MEV of Rebasing Tokens using Phalcon Fork
In this tutorial, I will show you how to use Phalcon Fork to understand the MEV of rebasing tokens. The code is released on the GitHub.
The transactions of the Phalcon Fork
The definition of MEV has been changed in the past years. Previously, it meant the Miner Extract Value. However, MEV means Maximal Extractable Value that can be extracted by manipulating the transaction order inside a block. You can refer to the document for more information.
The rebasing token means the supply is adjusted based on some pre-defined algorithm. One rebasing method is to adjust the balance of each token holder perilously.
Lido's staked Ether token
stETH
is a rebasing ERC20 rebasing token. The token holder's balance is changed daily. The balance of stETH of a token holder is calculated using the following formula.balanceOf(account) = shares[account] * totalPooledEther / totalShares
In summary, the balance of stETH token holders will be increased daily.
When swapping token X to token Y inside the Uniswap v2 pool, users usually interact with the Uniswap router contract, which will find the pool contract of the tokens. However, a user can directly invoke the
swap
function inside a pool.However, how to determine the price of the tokens? Uniswap uses the constant product formula for this purpose.
means the balance of token X inside the pool after and before the swap.
means the balance of token Y after and before the swap.
However, each swap will have a fee (0.003 currently). So, the formula becomes the following (suppose we use Token X to swap Token Y -- X is the token into the pool, and Y is the token out of the pool).
The
means the number of token X that has been transferred inside the pool, and
means the number of token Y that will be swapped out.
In the Uniswap pool smart contract, it uses two variables
_reserve0
and reserve1
to denote the balance before the swap, and use the balance0
and balance1
to denote the balance after the swap (but do not consider the fee yet -- we can ignore it here). 1
// this low-level function should be called from a contract which performs important safety checks
2
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
3
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
4
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
5
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
6
7
uint balance0;
8
uint balance1;
9
{ // scope for _token{0,1}, avoids stack too deep errors
10
address _token0 = token0;
11
address _token1 = token1;
12
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
13
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
14
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
15
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
16
balance0 = IERC20(_token0).balanceOf(address(this));
17
balance1 = IERC20(_token1).balanceOf(address(this));
18
}
19
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
20
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
21
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
22
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
23
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
24
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
25
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
26
}
27
28
_update(balance0, balance1, _reserve0, _reserve1);
29
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
30
}
The formula
calculates the input token X inside the pool (line 19 in the code). This works perfectly in most cases since the reserves recorded inside the pool are equal to the balances of tokens before the swap.
However, for the rebasing token like
stETH
the token balance increased periodically, which means the balance of stETH
in this pool will be increased. This creates a MEV opportunity because the rebasing creates a inside the pool, which could be used to swap token Y out by anyone.
This opportunity has been observed and actively taken. Let me use a transaction as an example. The hash of this transaction is
0x4b94095fe2c0014156d6b400e4fc405895cb508ab2802fa27b6135f939de5725
.Through the balance change of this transaction in Phalcon Explorer, we can find that the address 0x7024960031000000e9f80000790090f61732277b got a profit of
0.01
Ether in this transaction, and paid 0.36
Ether as the bribe fee to FlashBot.
Why this transaction can make profit?

Because the balance of stETH is bigger than
reserve0
in the pool, the 0x7024 address invokes the swap
function to use the increased balance (0.38 stETH) to swap WETH.3,950,436,974,827,841,051,750 - 3,950,055,013,348,304,070,261 = 381,961,479,536,981,489
As a result, 0.379 WETH was swapped out. The address keeps 0.01 Ether and bribed 0.36 Ether to FlashBot.
From the on-chain data, we can only observe the flow of the MEV transaction. However, we do not have the source code of the contract (0x7024).
To fully understand this process, we can develop our own MEV contract and debug it in Phalcon Fork.
First, we need to create a Fork inside the dashboard of Phalcon Fork.

We use Foundry as the development framework. Add the following information to the configuration file (
foundry.toml
).[rpc_endpoints]
phalcon = "${PHALCON_FORK_RPC_URL}"
[etherscan]
phalcon = { key = "${ETHERSCAN_API_KEY}", url = "${VERIFIER_URL}"}
Add a
.env
file to store the Phalcon Fork RPC, and API_KEY used to verify the contract. If you do not know the values, you can click the Configuration
button inside the Fork. The following screenshot shows the detailed RPC, API_KEY, and verifier URL. 
The source code of the smart contract is in the following.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./Interface.sol";
// import "forge-std/console.sol";
// Be cautious!! This is an example contract. DO NOT use it in a production environment.
contract MEVExample {
IUniswapV2Pair stETH_WETH_pair = IUniswapV2Pair(0x4028DAAC072e492d34a3Afdbef0ba7e35D8b55C4);
address owner;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
constructor() {
owner = msg.sender;
}
function trigger() external onlyOwner {
//query the balance and reserve of the pool
address token0 = stETH_WETH_pair.token0();
address token1 = stETH_WETH_pair.token1();
(uint112 reserve0, uint112 reserve1, ) = stETH_WETH_pair.getReserves();
uint256 balance0 = IERC20(token0).balanceOf(address(stETH_WETH_pair));
uint256 balance1 = IERC20(token1).balanceOf(address(stETH_WETH_pair));
// console.log("[stETH] reserve : balance", reserve0, balance0);
// console.log("[WETH] reserve : balance", reserve1, balance1);
require (balance0 > reserve0, "Balance should be bigger than Reserve!");
// calculate how many tokens can be swapped out
// this is the amount0In
uint amount0In = balance0 - reserve0;
uint amount1Out = balance1 - (uint(reserve0)*(reserve1)*(1000**2) / ((balance0 * 1000 - 3 * amount0In)))/1000;
// console.log("[stETH] out ", amount1Out);
// perform the swap
stETH_WETH_pair.swap(0, amount1Out - 1, msg.sender, "");
}
}
We use the Foundry script to deploy and trigger the contract.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import {MEVExample} from "../src/MEV.sol";
contract MEVScript is Script {
MEVExample public mevExample;
function setUp() public {
}
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
mevExample = new MEVExample();
mevExample.trigger();
vm.stopBroadcast();
}
}
The
PRIVATE_KEY
is configured inside the .env
file. We need to import it before executing the script.source .envforge script script/MEV.s.sol:MEVScript --rpc-url $PHALCON_FORK_RPC_URL --broadcast --verify -vvvv
This will build, deploy, verify the contract, and invoke the
trigger
function to issue the MEV transaction.
From the Phalcon Scan, we can find the deployed contract and issued transactions. Also, we can use the Phalcon Explorer to understand and debug the transaction.

In this document, we show an example of developing an MEV contract and deploying it inside Phalcon Fork.
In fact, using Phalcon Fork, you can develop and write your contract with real mainnet states. You can ensure everything is correct before deploying it on the mainnet. Besides, Phalcon Fork can be integrated into the CD pipeline through the API.
Last modified 2mo ago