- Published on
【Free MEV系列】| Subway 夹击策略
- Authors
- Name
- thinkingchaindotapp
subway
合约
整个文件夹下面,核心就是一个MEV bot的合约:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import "./interface/IERC20.sol";
import "./lib/SafeTransfer.sol";
contract Sandwich {
using SafeTransfer for IERC20;
address internal immutable owner;
// transfer(address,uint256)
bytes4 internal constant ERC20_TRANSFER_ID = 0xa9059cbb;
// swap(uint256,uint256,address,bytes)
bytes4 internal constant PAIR_SWAP_ID = 0x022c0d9f;
receive() external payable {}
constructor(address _owner) {
owner = _owner;
}
// *** Receive profits from contract *** //
function recoverERC20(address token) external {
require(msg.sender == owner, "not owner");
IERC20(token).safeTransfer(
msg.sender,
IERC20(token).balanceOf(address(this))
);
}
/*
Fallback function where you do your frontslice and backslice
NO UNCLE BLOCK PROTECTION IN PLACE, USE AT YOUR OWN RISK
Payload structure (abi encodePacked)
- token: address - Address of the token you're swapping
- pair: address - Univ2 pair you're sandwiching on
- amountIn: uint128 - Amount you're giving via swap
- amountOut: uint128 - Amount you're receiving via swap
- tokenOutNo: uint8 - Is the token you're giving token0 or token1? (On uniswap V2 pair)
Note: This fallback function generates some dangling bits
*/
fallback() external payable {
// Assembly cannot read immutable variables
address memOwner = owner;
assembly {
// only owner
if iszero(eq(caller(), memOwner)) {
// Ohm (3, 3) makes your code more efficient
revert(3, 3)
}
// bytes20
let token := shr(96, calldataload(0x00))
// bytes20
let pair := shr(96, calldataload(0x14))
// uint128
let amountIn := shr(128, calldataload(0x28))
// uint128
let amountOut := shr(128, calldataload(0x38))
// uint8
let tokenOutNo := shr(248, calldataload(0x48))
// **** calls token.transfer(pair, amountIn) ****
// transfer function signature
mstore(0x7c, ERC20_TRANSFER_ID)
// destination
mstore(0x80, pair)
// amount
mstore(0xa0, amountIn)
let s1 := call(sub(gas(), 5000), token, 0, 0x7c, 0x44, 0, 0)
if iszero(s1) {
revert(3, 3)
}
// ************
/*
calls pair.swap(
tokenOutNo == 0 ? amountOut : 0,
tokenOutNo == 1 ? amountOut : 0,
address(this),
new bytes(0)
)
*/
// swap function signature
mstore(0x7c, PAIR_SWAP_ID)
// tokenOutNo == 0 ? ....
switch tokenOutNo
case 0 {
mstore(0x80, amountOut)
mstore(0xa0, 0)
}
case 1 {
mstore(0x80, 0)
mstore(0xa0, amountOut)
}
// address(this)
mstore(0xc0, address())
// empty bytes
mstore(0xe0, 0x80)
let s2 := call(sub(gas(), 5000), pair, 0, 0x7c, 0xa4, 0, 0)
if iszero(s2) {
revert(3, 3)
}
}
}
}
使用内联汇编在fallback中书写了功能:将代币转到池子(token0/token1,双向),然后swap。
Bot代码
算法
这个仓库的核心是做三明治攻击,场景是:
- Attacker先执行
WETH -> TOKEN
交易,推高 token 的价格。 - Victim执行
WETH -> TOKEN
交易,进一步推高 token 的价格。 - Attacker执行
TOKEN -> WETH
交易,卖出 token 获利。
为了盈利,设计了一个二分查找的算法,如下图所示(你可以在此链接中找到原出处):
算法的逻辑:
- Victim有一个最小的输出金额
a
,我们在操纵价格之后,不得使得他的输出小于a
- 假设Victim的实际输出为金额
b
,b
必须大于等于a
,并且b
越贴近于a
,我们的获利越大 - Attacker设置一个缓冲区tolerance,代表我们期待
b
需要满足的范围。tolerance的范围设置为:[a
,a
* 1.01],我们允许1%的缓冲范围 - 算法需要不断调整
Attacker's ETH amontIn
,使得b
位于tolerance缓冲区中 - 当
b
位于tolerance时,也要满足profit大于0,才算完成搜索
分析一下图中的两种情况(其实还有其他情况,就不多举例了):
- scenario 1
- Step 1:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance的右边,说明我们还可以抬高Token价格增加获利空间,可以在此基础上继续抬高价格 - Step 2:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance的右边,说明我们还可以抬高Token价格增加获利空间,可以在此基础上继续抬高价格 - Step 3:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance的左边,说明我们使Token价格抬高得太多了导致Victim不愿意了,应该在此基础上拉低价格 - Step 4:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance中:如果profit大于0,则推出,否则继续迭代直到profit大于0
- Step 1:attacker输入一定数量的ETH之后,Victim的最终输出金额
- scenario 2
- Step 1:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance的左边,说明我们使Token价格抬高得太多了导致Victim不愿意了,应该在此基础上拉低价格 - Step b:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance的左边,说明我们使Token价格抬高得太多了导致Victim不愿意了,应该在此基础上拉低价格 - Step c:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance的右边,说明我们还可以抬高Token价格增加获利空间,可以在此基础上继续抬高价格 - Step d:attacker输入一定数量的ETH之后,Victim的最终输出金额
b
位于tolerance中:如果profit大于0,则推出,否则继续迭代直到profit大于0
- Step 1:attacker输入一定数量的ETH之后,Victim的最终输出金额
分析代码:
- 这是核心的二分查找代码
export const binarySearch = (
left, // Lower bound
right, // Upper bound
calculateF, // Generic calculate function
passConditionF, // Condition checker
tolerance = parseUnits("0.01") // Tolerable delta (in %, in 18 dec, i.e. parseUnits('0.01') means left and right delta can be 1%)
) => {
- 向外暴露的、我们所使用的函数,包装了
binarySearch()
export const calcSandwichOptimalIn = (
userAmountIn,
userMinRecvToken,
reserveWeth,
reserveToken
) => {
- 计算、查看三明治攻击的state
export const calcSandwichState = (
optimalSandwichWethIn,
userWethIn,
userMinRecv,
reserveWeth,
reserveToken
) => {
flashbot
- 如果存在错误则捕获,没有则返回原来的结果
export const sanityCheckSimulationResponse = (sim) => {
- 模拟执行
export const callBundleFlashbots = async (signedTxs, targetBlockNumber) => {
- 将交易对象序列化为原始交易数据(十六进制字符串)
export const getRawTransaction = (tx) => {
- 发送Bundles交易到flashbot
export const sendBundleFlashbots = async (signedTxs, targetBlockNumber) => {
main
流程如下:
1.不断监听memory pool中的status为pending的交易,然后开始处理它
2.从pending的交易中解析出详情:(1)交易存在(我们只监听ETH=>Token的交易);(2)交易是和UniswapV2Router交互;(3)解码calldata;(4)交易还没到deadline
3.根据victim的交易详情,计算出一些准备数据:(1)我们用WETH换成token,token的最少数量;(2)Pair地址;(3)Reserve数量;
4.算法开始,找到profit
5.根据profit的结果,计算出我们三明治交易的输入内容,然后准备
6.准备区块状态信息
7.打包三明治交易:(1)我们的第一笔交易,套高价格;(2)Victim的交易;(3)我们的第二笔交易,换回,拿到利润
8.flashbot模拟交易,看看是否可行
9.计算gas消耗,还有贿赂费,看看是否盈利
10.一切就绪,发射🚀三明治交易