分析MEV夹子机器人的技术细节

初始流动池状态:

机器人买入:

  • 此时流动池中的状态
  • A1: 7.07 USDT
  • B1: 12.145 TXXC
  • 此时的价格 7.07 / 12.145 = 0.5821 USDT/TXXC

我的添加流动性交易:

  • 该交易区块的第2个位置

  • 源码: https://bscscan.com/address/0x10ed43c718714eb63d5aa57b78b54704e256024e#code

  • 详细代码分析,见文末

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired, // 希望投入的代币 A 的数量,但实际投入量可能会因为比例调整而不同
    uint amountBDesired, // 希望投入的代币 B 的数量,但实际投入量可能会因为比例调整而不同
    uint amountAMin, // 滑点保护,用户愿意接受的最少代币 A 的数量, 确保用户不会因为价格波动而损失太多代币A
    uint amountBMin, // 滑点保护,用户愿意接受的最少代币 B 的数量,确保用户不会因为价格波动而损失太多代币B
    address to, // 流动性代币接收者的地址。流动性代币是代表用户在流动池中所有权的代币。
    uint deadline // 交易的最后期限(时间戳)。确保交易在指定时间内完成,否则交易将被取消。
    )
  • 交易: https://bscscan.com/tx/0x3d00cad4557eecd49906c1874c838576928d238a774f10b118d44bf96d071d74

  • 计算:

    • amountADesired: 8.998834461493163430 USDT
    • amountBDesired: 7.73000000000000000000 TXXC
    • 因此amountBOptimal = amountADesired * B1 / A1 = 8.998 * 12.1455 / 7.07 = 15.456
    • 满足 if (amountBOptimal <= amountBDesired) , 所以
      • 因此 (amountA, amountB) = (amountADesired, amountBOptimal);
      • (amountA, amountB) = (8.998, 15.456);
  • 其中 quote函数, 按照等比例增加, 这里只能近似:

    • A2: 7.07 + 8.99 = 16.06 USDT
    • B2: 12.145 + 15.3 = 27.445 TXXC
    • 此时价格 0.5854 USDT/TXXC

MEV机器人的卖出

  • 此时MEV机器人全部卖出,
    • B3 = 27.45 + 66.5 + 3.5 = 97.45
    • A3 = k / B3 = (16.06*27.445) / 97.45 = 4.523
    • 此时的价格: A3 / B3 = 4.523 / 97.45 = 0.0464 USDT/TXXC
  • MEV机器人的获利:

    • USDT: 16.06 - 4.523 - 6.07 = 5.467 USDT
    • BNB手续费: 0.00475 BNB , 0.0047 * 700 = 3.324 U
      • 购买交易手续费:0.00245 BNB
        • gas price: 11Gwei , 一般是 1Gwei, 所以能排在第0个位置,
      • 购买交易平台手续费: 0.002 BNB
      • 卖出交易手续费:0.0003 BNB
    • 合计: 5.467 - 3.324 = 2.1423
    • MEV通过这一次交易,净赚2.14 U
  • 我损失的:

    • USDT- -5.467
    • BNB: +0.0026 BNB, 约 1.82U
      • 池子是我建的,手续费归我
    • 合计: -3.647U
    • 我这笔交易损失: 3.647 U

分析原因

  • 添加流动性交易,没有设置滑点保护参数, amountAMinamountBMin:
    • 源码 https://bscscan.com/address/0x10ed43c718714eb63d5aa57b78b54704e256024e#code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
      function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
      require(amountA > 0, 'PancakeLibrary: INSUFFICIENT_AMOUNT');
      require(reserveA > 0 && reserveB > 0, 'PancakeLibrary: INSUFFICIENT_LIQUIDITY');
      amountB = amountA.mul(reserveB) / reserveA;
      }

      // **** ADD LIQUIDITY ****
      function _addLiquidity(
      address tokenA,
      address tokenB,
      uint amountADesired,
      uint amountBDesired,
      uint amountAMin,
      uint amountBMin
      ) internal virtual returns (uint amountA, uint amountB) {
      // create the pair if it doesn't exist yet
      if (IPancakeFactory(factory).getPair(tokenA, tokenB) == address(0)) {
      IPancakeFactory(factory).createPair(tokenA, tokenB);
      }
      (uint reserveA, uint reserveB) = PancakeLibrary.getReserves(factory, tokenA, tokenB);
      if (reserveA == 0 && reserveB == 0) {
      (amountA, amountB) = (amountADesired, amountBDesired);
      } else {
      // 计算 B的最小数量, 满足 A * B = k = A' * B' 的恒常等式
      // B' = A' * B
      uint amountBOptimal = PancakeLibrary.quote(amountADesired, reserveA, reserveB);

      // 如果 B的最有数量 小于 用户愿意投入的
      if (amountBOptimal <= amountBDesired) {

      // 滑点保护
      require(amountBOptimal >= amountBMin, 'PancakeRouter: INSUFFICIENT_B_AMOUNT');

      (amountA, amountB) = (amountADesired, amountBOptimal);
      } else {

      // 计算 A的最优数量
      uint amountAOptimal = PancakeLibrary.quote(amountBDesired, reserveB, reserveA);

      // 必须满足: A的
      assert(amountAOptimal <= amountADesired);

      // 滑点保护
      require(amountAOptimal >= amountAMin, 'PancakeRouter: INSUFFICIENT_A_AMOUNT');

      (amountA, amountB) = (amountAOptimal, amountBDesired);
      }
      }
      }
      function addLiquidity(
      address tokenA,
      address tokenB,
      uint amountADesired,
      uint amountBDesired,
      uint amountAMin, // 滑点保护
      uint amountBMin, // 滑点保护
      address to,
      uint deadline
      ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
      (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
      address pair = PancakeLibrary.pairFor(factory, tokenA, tokenB);
      TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
      TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
      liquidity = IPancakePair(pair).mint(to);
      }

关于MEV的一些问题?

参考: https://learnblockchain.cn/article/3163

  • 交易顺序如何确定?
    • MEV在第一笔交易设置了非常搞的gas price
    • 矿工通过重组交易的执行顺序,可以获取收益
      • 至于具体重组交易顺序的算法,需要进一步研究
  • 如何防止MEV夹子机器人?
    • 设置合适的滑点,
      • 如果是添加流动性,要设置 amountAMin, amountBMin
    • 使用私密交易,用第三方的工具

智能合约课程大纲

登链社区的培训课程大纲, 非常全面,可以借鉴学习

https://learnblockchain.cn/openspace/1

课程大纲

夯实基础

  • 比特币、以太坊核心技术原理,核心概念:钱包账号、交易调用、GAS 机制
  • Remix & Solidity 语言特性:数据类型、函数、库、事件、异常处理、OpenZeppelin
  • 开发工具:MetaMask、Hardhat、Foundry,如何进行编译、部署、代码验证
  • 测试:Foundry作弊码使用、Fork 链模拟测试、模糊测试
  • ERC 标准介绍及实战:ERC20、ERC777、EIP2612、ERC721、ERC1155 及 SBT
  • 理解合约 ABI
  • 跟踪链上数据:解析合约事件与TheGraph 使⽤
  • 使用 Oracle 预言机、Keeper 服务,进行链上链下自动化交互
  • 前端 web3 SDK 集成:ethers.js ,viem,wagmi ,walletconnect 原理及应用

合约开发进阶

  • 探究升级原理及可能遇到问题,实践合约升级模式:透明代理及 UUPS
  • 理解底层调用call、delegatecall,什么时候用、该如何用,有什么风险;
  • 理解 Multicall 解决什么问题
  • 离线签名的作用、如何安全的应用离线签名、线上验证
  • 智能合约钱包、多签钱包、AA 钱包
  • 跨链交互
  • 探究 EVM ,理解合约字节码
  • Solidity 合约数据存储布局
  • Gas 优化技巧:数据结构优化和使用、链上与链下权衡
  • 高级安全技术及漏洞挑战:重入攻击、不安全的随机数、权限漏洞
  • 大量 CTF 挑战实战,培养优秀代码思维
  • Flashbots 应用

DeFi 算法

  • 核心 DEFI 协议分析: Uniswap、 Compound、AAVE 、MakerDAO 等
  • AMM DEX 实践
  • 借贷算法 Compound
  • 质押分红算法
  • 算法稳定币

Rollup 二层

  • Op-Stack 架构、Rollup 流程、跨链调用
  • 深入理解 ETH 和 ERC20 充值提现
  • 本地启动 op-Stack 测试网
  • 开发自己的的 Layer2 链
  • 模块化区块链、以太坊 DA(EIP4844 )与 Celestia 等
  • EigenLayer 重质押与 EigenDA

应用开发

  • 实现一个你自己的创意想法(学员组队完成)
  • 构建一个 DEX 、 Token 质押应用
  • 构建 NFT 市场
  • 构建抽象账户(AA)钱包
  • 构建一个区块链浏览器

基于BodingCurve价格发现的代币

基于BondingCurve价格发现的代币

相关链接

关于抢跑问题

  • 通过设置一个最大的gas price,可以避免抢跑问题
1
2
3
4
5
6
7
8
9
10
11
12
13

contract CappedGasPrice is Ownable {
uint256 public maxGasPrice = 1 * 10**18; // Adjustable value

modifier validGasPrice() {
require(tx.gasprice <= maxGasPrice, "Transaction gas price cannot exceed maximum gas price.");
_;
}

function setMaxGasPrice(uint256 gasPrice) public onlyOwner {
maxGasPrice = gasPrice;
}
}

P2P技术(UDP)打洞

其中最终核心的就是:

端点在不同的NAT之后

假设客户端A和客户端B的地址都是内网地址,且在不同的NAT后面. A、B上运行的P2P应用程序和服务器S都使用了UDP端口1234,A和B分别初始化了 与Server的UDP通信,地址映射如图所示:

1
2
3
4
5
6
7
8
9
10
11
12
                            Server S
18.181.0.31:1234
|
|
+----------------------+----------------------+
| |
NAT A NAT B
155.99.25.11:62000 138.76.29.7:31000
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234

现在假设客户端A打算与客户端B直接建立一个UDP通信会话. 如果A直接给B的公网地址138.76.29.7:31000发送UDP数据,NAT B将很可能会无视进入的 数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,而最初只与S建立过会话. B往A直接发信息也类似.

假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息. A往B的输出信息会导致NAT A打开 一个A的内网地址与与B的外网地址之间的新通讯会话, B往A亦然. 一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯, 而无须再通过引导服务器S了.

UDP打洞技术有许多有用的性质. 一旦一个的P2P链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞, 极大减少了服务器的负载. 应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下 也一样能建立通信链路.

  • 服务端: https://github.com/youngqqcn/P2P-Over-MiddleBoxes-Demo/blob/master/p2pchat/server.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45

    void on_message(int sock, endpoint_t from, Message msg) {
    log_debug("RECV %d bytes FROM %s: %s %s", msg.head.length,
    ep_tostring(from), strmtype(msg.head.type), msg.body);
    switch(msg.head.type) {


    case MTYPE_LOGIN: // 登录, 记录客户端的地址
    {
    if (0 == eplist_add(g_client_pool, from)) {
    log_info("%s logged in", ep_tostring(from));
    udp_send_text(sock, from, MTYPE_REPLY, "Login success!");
    } else {
    log_warn("%s failed to login", ep_tostring(from));
    udp_send_text(sock, from, MTYPE_REPLY, "Login failed");
    }
    }
    break;

    // ....

    case MTYPE_PUNCH: // UDP打洞核心逻辑
    {
    endpoint_t other = ep_fromstring(msg.body);
    log_info("punching to %s", ep_tostring(other));

    // 向目的地址发送打洞PUNCH消息, 并将源地址作为消息体,发给目的地址
    udp_send_text(sock, other, MTYPE_PUNCH, ep_tostring(from));

    // 向源地址发送一个消息, 源地址收到不会回复
    udp_send_text(sock, from, MTYPE_TEXT, "punch request sent");

    }
    break;
    case MTYPE_PING:
    udp_send_text(sock, from, MTYPE_PONG, NULL);
    break;
    case MTYPE_PONG:
    break;
    default:
    udp_send_text(sock, from, MTYPE_REPLY, "Unkown command");
    break;
    }
    }

  • 客户端:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48

    void on_message(endpoint_t from, Message msg) {
    log_debug("RECV %d bytes FROM %s: %s %s", msg.head.length,
    ep_tostring(from), strmtype(msg.head.type), msg.body);
    // from server
    if (ep_equal(g_server, from)) {
    switch (msg.head.type) {
    case MTYPE_PUNCH: // 收到服务端的打洞请求,
    {
    endpoint_t peer = ep_fromstring(msg.body);
    log_info("%s on call, replying...", ep_tostring(peer));

    // 给源地址回复一条消息,
    udp_send_text(g_clientfd, peer, MTYPE_REPLY, NULL);
    }
    break;
    case MTYPE_REPLY:
    log_info("SERVER: %s", msg.body);
    break;
    default:
    break;
    }
    return;
    }
    // from peer
    switch (msg.head.type) {
    case MTYPE_TEXT:
    log_info("Peer(%s): %s", ep_tostring(from), msg.body);
    break;
    case MTYPE_REPLY: // UDP打洞打通了
    log_info("Peer(%s) replied, you can talk now", ep_tostring(from));
    eplist_add(g_peers, from);
    case MTYPE_PUNCH:
    /*
    * Usually we can't recevie punch request from other peer directly,
    * but it could happen when it come after we reply the punch request from server,
    * or there's a tunnel already.
    * */
    log_info("Peer(%s) punched", ep_tostring(from));
    udp_send_text(g_clientfd, from, MTYPE_TEXT, "I SEE YOU");
    break;
    case MTYPE_PING:
    udp_send_text(g_clientfd, from, MTYPE_PONG, NULL);
    log_info("Peer(%s) pinged", ep_tostring(from));
    default:
    break;
    }
    }

以上的代码我在本地和2台服务做了测试,成功:

1
2
3
4
5
            Server(腾讯云服务器)


ClientA(本机) ClientB(aws服务器 )

strtok源码

strtok源码分析

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
#include <stdio.h>

int main () {
char str[80] = "192.168.10.110:9000";
char *pszHost = strtok(str, ":");
char *pszPort = strtok(NULL, ":");
printf("%s\n", pszHost);
printf("%s\n", pszPort);
return(0);
}

输出

1
2
192.168.10.110
9000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* Parse S into tokens separated by characters in DELIM.
If S is NULL, the last string strtok() was called with is
used. For example:
char s[] = "-abc-=-def";
x = strtok(s, "-"); // x = "abc"
x = strtok(NULL, "-="); // x = "def"
x = strtok(NULL, "="); // x = NULL
// s = "abc\0=-def\0"
*/
char *
strtok (char *s, const char *delim)
{
static char *olds; // 保留上一次的位置
return __strtok_r (s, delim, &olds);
}



/* Parse S into tokens separated by characters in DELIM.
If S is NULL, the saved pointer in SAVE_PTR is used as
the next starting point. For example:
char s[] = "-abc-=-def";
char *sp;
x = strtok_r(s, "-", &sp); // x = "abc", sp = "=-def"
x = strtok_r(NULL, "-=", &sp); // x = "def", sp = NULL
x = strtok_r(NULL, "=", &sp); // x = NULL
// s = "abc\0-def\0"
*/
char *
__strtok_r (char *s, const char *delim, char **save_ptr)
{
char *end;
if (s == NULL)
s = *save_ptr;
if (*s == '\0')
{
*save_ptr = s;
return NULL;
}
/* Scan leading delimiters. */
s += strspn (s, delim);
if (*s == '\0')
{
*save_ptr = s;
return NULL;
}
/* Find the end of the token. */
end = s + strcspn (s, delim);
if (*end == '\0')
{
*save_ptr = end;
return s;
}
/* Terminate the token and make *SAVE_PTR point past it. */
*end = '\0'; // 设置结束符
*save_ptr = end + 1; // 指针移动到下一个位置
return s;
}

  • Copyrights © 2021-2024 youngqqcn

请我喝杯咖啡吧~