Defi安全-Mono攻击事件分析--etherscan+phalcon

MonoX攻击事件相关信息

在Ethereum和Polygon网络都发生了,攻击手段相同,以Ethereum为例进行分析:

  • 攻击者地址:MonoX Finance Exploiter | Address 0xecbe385f78041895c311070f344b55bfaa953258 | Etherscan

  • 攻击合约:Contract Address 0xf079d7911c13369e7fd85607970036d2883afcfd | Etherscan

  • 攻击交易:Ethereum Transaction Hash (Txhash) Details | Etherscan

  • 漏洞合约:Monoswap | Address 0x66e7d7839333f502df355f5bd87aea24bac2ee63 | Etherscan

Monox代码分析及攻击流程讲解

Monox介绍:

与Uniswap不同,其使用的是单边代币池模型,其使用vCash稳定币与AMM提供的代币创建虚拟的交易对。Monox创建的是代币-vCash交易对,添加流动性的时候,只需添加代币,进行任意代币兑换,兑换方式为:代币A -- vCash -- 代币B

攻击原理及过程:

极大地提高Monoswap中Mono代币的价格,后将拥有的Mono代币通过Monoswap换取代币。

具体步骤,查看phalcon上攻击交易的调用序列进行分析

  1. 前置阶段

image-20231223171211651

  • 首先调用WETHdeposit()函数,向WETH中存入0.1WETH
  • 随后调用approve()函数,向Monoswap进行授权,以便后续代币兑换正常进行(在foundry中写测试函数时,很容易遗忘approve这点)
  • 随后调用Monoswap的swapExactTokenForToken()函数,将0.1个WETH换成一定数量的Mono(该函数如何实现,可见漏洞合约Monoswap)
  • 调用Monoswap的pools()函数,具体后续介绍,获得Mono代币在Monoswap中的pid
  • 根据pid调用Monoxpool中的totalSupplyOf()函数,查询Mono-vCash池子中作为LP流动性证明的Mono总量。
  1. 移除用户流动性

image-20231223215453923

在Monox的官方界面可以看到给Mono代币提供代币流动的用户地址,这里从交易序列中可以很明显发现一个漏洞,别的用户的流动性,攻击者竟然可以任意移除

在Monoswap源码中可以很明显发现,并没有流动性所有者进行相应的校验

function _removeLiquidity (address _token, uint256 liquidity,
    address to) view public returns(
    uint256 poolValue, uint256 liquidityIn, uint256 vcashOut, uint256 tokenOut) {
    
    require (liquidity>0, "MonoX:BAD_AMOUNT");
    uint256 tokenBalanceVcashValue;
    uint256 vcashCredit;
    uint256 vcashDebt;
    PoolInfo memory pool = pools[_token];
    IMonoXPool monoXPoolLocal = monoXPool;
    uint256 lastAdded = monoXPoolLocal.liquidityLastAddedOf(pool.pid, msg.sender);
    
    require((lastAdded + (pool.status == PoolStatus.OFFICIAL ? 4 hours : pool.status == PoolStatus.LISTED ? 24 hours : 0)) <= block.timestamp, "MonoX:WRONG_TIME"); // Users are not allowed to remove liquidity right after adding
    address topLPHolder = monoXPoolLocal.topLPHolderOf(pool.pid);
    require(pool.status != PoolStatus.LISTED || msg.sender != topLPHolder || pool.createdAt + 90 days < block.timestamp, "MonoX:TOP_HOLDER & WRONG_TIME"); // largest LP holder is not allowed to remove LP within 90 days after pool creation

    (poolValue, tokenBalanceVcashValue, vcashCredit, vcashDebt) = getPool(_token);
    uint256 _totalSupply = monoXPool.totalSupplyOf(pool.pid);

    liquidityIn = monoXPool.balanceOf(to, pool.pid)>liquidity?liquidity:monoXPool.balanceOf(to, pool.pid);
    uint256 tokenReserve = IERC20(_token).balanceOf(address(monoXPool));
    
    if(tokenReserve < pool.tokenBalance){
      tokenBalanceVcashValue = tokenReserve.mul(pool.price)/1e18;
    }

    if(vcashDebt>0){
      tokenReserve = (tokenBalanceVcashValue.sub(vcashDebt)).mul(1e18).div(pool.price);
    }

    // if vcashCredit==0, vcashOut will be 0 as well
    vcashOut = liquidityIn.mul(vcashCredit).div(_totalSupply);
    tokenOut = liquidityIn.mul(tokenReserve).div(_totalSupply);
  }

攻击者发现三个主要提供流动性的用户,先调用Monoxpool的balanceOf()函数查看地址在Monoswap中的Mono数量,后调用移除流动性函数,使得池子中的Mono为0.

  1. 添加流动性

攻击者自己添加极少的Mono代币到Monoswap中,获得927个LP,为后续拉升Mono的价格做准备

image-20231223220301534

  1. 拉高Mono代币在Monoswap中的价格

image-20231223220818186

攻击交易中,重复了55次上述行为

先是调用Monoswap中的pools()函数,从中我们可以看出solidity中这种mapping映射的获得,是通过调用函数的形式活动,可以看一下该函数返回的函数类型:

mapping (address => PoolInfo) public pools;
  struct PoolInfo {
    uint256 pid;
    uint256 lastPoolValue;
    address token;
    PoolStatus status;
    uint112 vcashDebt;
    uint112 vcashCredit;
    uint112 tokenBalance;
    uint256 price; // over 1e18
    uint256 createdAt; // timestamp
  }

这里重点关注的是我们可以通过调用该函数获得该代币在Monoswap中的tokenBalance余额和price当前价格,攻击交易这里主要想获得池子中的tokenBalance余额。

随后查看攻击者先前用0.1个WETH兑换的Mono代币的余额,即还剩多少个

随后最关键的步骤调用Monoswap的swapExactTokenForToken()函数,这个函数的功能与uniswap很像,顾名思义,将精准数量的代币兑换成一定数量的另一种代币,这里我们能够很明显发现,参数tokenIntokenOut都是Mono,这就是攻击手段!

所以肯定是该函数中存在漏洞,导致Mono代币价格的拉高。进入函数中看一下。

  function swapExactTokenForToken(
    address tokenIn,
    address tokenOut,
    uint amountIn,
    uint amountOutMin,
    address to,
    uint deadline
  ) external virtual ensure(deadline) returns (uint amountOut) {
    amountOut = swapIn(tokenIn, tokenOut, msg.sender, to, amountIn);
    require(amountOut >= amountOutMin, 'MonoX:INSUFF_OUTPUT');
  }
  
  function swapIn (address tokenIn, address tokenOut, address from, address to,
      uint256 amountIn) internal lockToken(tokenIn) returns(uint256 amountOut)  {

    address monoXPoolLocal = address(monoXPool);

    amountIn = transferAndCheck(from,monoXPoolLocal,tokenIn,amountIn); 
    
    // uint256 halfFeesInTokenIn = amountIn.mul(fees)/2e5;

    uint256 tokenInPrice;
    uint256 tokenOutPrice;
    uint256 tradeVcashValue;
    
    (tokenInPrice, tokenOutPrice, amountOut, tradeVcashValue) = getAmountOut(tokenIn, tokenOut, amountIn);

    uint256 oneSideFeesInVcash = tokenInPrice.mul(amountIn.mul(fees)/2e5)/1e18;

    // trading in
    if(tokenIn==address(vCash)){
      vCash.burn(monoXPoolLocal, amountIn);
      // all fees go to the other side
      oneSideFeesInVcash = oneSideFeesInVcash.mul(2);
    }else{
      _updateTokenInfo(tokenIn, tokenInPrice, 0, tradeVcashValue.add(oneSideFeesInVcash), 0);
    }

    // trading out
    if(tokenOut==address(vCash)){
      vCash.mint(to, amountOut);
    }else{
      if (to != monoXPoolLocal) {
        IMonoXPool(monoXPoolLocal).safeTransferERC20Token(tokenOut, to, amountOut);
      }
      _updateTokenInfo(tokenOut, tokenOutPrice, tradeVcashValue.add(oneSideFeesInVcash), 0, 
        to == monoXPoolLocal ? amountOut : 0);
    }

    if(pools[tokenIn].vcashDebt > 0 && pools[tokenIn].status == PoolStatus.OFFICIAL){
      _internalRebalance(tokenIn);
    }

    emit Swap(to, tokenIn, tokenOut, amountIn, amountOut, tradeVcashValue);
    
  }
  

swapIn函数较复杂,我们可以从后往前看,看到它有个_updateTokenInfo()函数,更新token的信息,看一下源码

  function _updateTokenInfo (address _token, uint256 _price,
      uint256 _vcashIn, uint256 _vcashOut, uint256 _ETHDebt) internal {
    uint256 _balance = IERC20(_token).balanceOf(address(monoXPool));
    _balance = _balance.sub(_ETHDebt);
    require(pools[_token].status!=PoolStatus.PAUSED,"MonoX:PAUSED");
    require(_balance <= uint112(-1));
    (uint initialPoolValue, , ,) = getPool(_token);
    pools[_token].tokenBalance = uint112(_balance);
    pools[_token].price = _price;

    // record last trade's block number in mapping: lastTradedBlock
    lastTradedBlock[_token] = block.number;

    _updateVcashBalance(_token, _vcashIn, _vcashOut);

    (uint poolValue, , ,) = getPool(_token);

    require(initialPoolValue <= poolValue || poolValue >= poolSizeMinLimit,
      "MonoX:MIN_POOL_SIZE");
    
    
  }

从代码中我们可以看出,将Monoswap池子中代币的数量和价格更新,其中代币的价格就是函数参数的tokenInPricetokenOutPrice,这两个参数都是通过getAmountOut()函数计算得到,进入该函数,分析源码:

function getAmountOut(address tokenIn, address tokenOut, 
    uint256 amountIn) public view returns (uint256 tokenInPrice, uint256 tokenOutPrice, 
    uint256 amountOut, uint256 tradeVcashValue) {
    require(amountIn > 0, 'MonoX:INSUFF_INPUT');
    
    uint256 amountInWithFee = amountIn.mul(1e5-fees)/1e5;
    address vcashAddress = address(vCash);
    uint tokenInPoolPrice = pools[tokenIn].price;
    uint tokenInPoolTokenBalance = pools[tokenIn].tokenBalance;

    if(tokenIn==vcashAddress){
      tradeVcashValue = amountInWithFee;
      tokenInPrice = 1e18;
    }else{
      require (tokenPoolStatus[tokenIn]==1, "MonoX:NO_POOL");
      // PoolInfo memory tokenInPool = pools[tokenIn];
      PoolStatus tokenInPoolStatus = pools[tokenIn].status;
      
      require (tokenInPoolStatus != PoolStatus.UNLISTED, "MonoX:POOL_UNLST");
      
      tokenInPrice = _getNewPrice(tokenInPoolPrice, tokenInPoolTokenBalance, 
        amountInWithFee, 0, TxType.SELL);
      tradeVcashValue = _getAvgPrice(tokenInPoolPrice, tokenInPrice).mul(amountInWithFee)/1e18;
    }

    if(tokenOut==vcashAddress){
      amountOut = tradeVcashValue;
      tokenOutPrice = 1e18;
    }else{
      require (tokenPoolStatus[tokenOut]==1, "MonoX:NO_POOL");
      // PoolInfo memory tokenOutPool = pools[tokenOut];
      PoolStatus tokenOutPoolStatus = pools[tokenOut].status;
      uint tokenOutPoolPrice = pools[tokenOut].price;
      uint tokenOutPoolTokenBalance = pools[tokenOut].tokenBalance;

      require (tokenOutPoolStatus != PoolStatus.UNLISTED, "MonoX:POOL_UNLST");
      
      amountOut = tradeVcashValue.add(tokenOutPoolTokenBalance.mul(tokenOutPoolPrice).div(1e18));
      amountOut = tradeVcashValue.mul(tokenOutPoolTokenBalance).div(amountOut);

      bool allowDirectSwap=directSwapAllowed(tokenInPoolPrice,tokenOutPoolPrice,tokenInPoolTokenBalance,tokenOutPoolTokenBalance,tokenOutPoolStatus,true);

      // assuming p1*p2 = k, equivalent to uniswap's x * y = k
      uint directSwapTokenOutPrice = allowDirectSwap?tokenInPoolPrice.mul(tokenOutPoolPrice).div(tokenInPrice):uint(-1);

      // prevent the attack where user can use a small pool to update price in a much larger pool
      tokenOutPrice = _getNewPrice(tokenOutPoolPrice, tokenOutPoolTokenBalance, 
        amountOut, 0, TxType.BUY);
      tokenOutPrice = directSwapTokenOutPrice < tokenOutPrice?directSwapTokenOutPrice:tokenOutPrice;

      amountOut = tradeVcashValue.mul(1e18).div(_getAvgPrice(tokenOutPoolPrice, tokenOutPrice));
    }
  }

通过上述代码可以得到,tokenInPricetokenOutPrice参数的计算都是通过_getNewPrice()函数,得到函数源码

  function _getNewPrice (uint256 originalPrice, uint256 reserve, 
    uint256 delta, uint256 deltaBlocks, TxType txType) pure internal returns(uint256 price) {
    if(txType==TxType.SELL) {
      // no risk of being div by 0
      price = originalPrice.mul(reserve)/(reserve.add(delta));
    }else{ // BUY
      price = originalPrice.mul(reserve).div(reserve.sub(delta));
    }
  }

通过,我们可以发现tokenIn代币,其TxType为SELL,tokenOut代币其Txtype为BUY。

故可分析,tokenIn代表先进行价格更新计算,originalPrice和reserve都是池子中原来保存的参数,其不会发生变动,相较于originalPrice价格,tokenInPrice变低了。

分析_getAvgPrice()函数,我们进一步可以分析得到trashVcashValue也变低了,其与toknInPrice呈相同趋势。

  function _getAvgPrice (uint256 originalPrice, uint256 newPrice) pure internal returns(uint256 price) {
    price = originalPrice.add(newPrice.mul(4))/5;
  }

随后,getAmountOut()函数正常执行,计算tokenOut代币的相关信息,分析_getNewPrice()函数,肯定可以得到的一个结论是相比于originalPrice也就是池子中代币的价格,tokenOutPrice变高的。

这时可以不用管其它参数的变化,这里最大的问题,就是这种同种代币的兑换,在swapIn()函数中,其先对tokenIn进行处理,更新代币相应的信息,但其后对tokenOut进行处理时,没有考虑前后兑换为同一种代币的情况,导致代币的价格被覆盖。

从上述分析中,可得到tokenOut的价格被抬升,tokenIn价格降低,但Mono的价格在兑换时,被覆盖,导致Mono价格异常增长。

对phalcon中兑换交易的参数分析可得,每次兑换的数量都是交易池中Mono的总量减去1,使得_getNewPrice()函数计算tokenOutPrice时,能够快速提升价格,这里也就不能理解第3步中添加流动性的时候,添加很少的Mono,确保攻击者有足够的余额拉高mono的价格。

  1. 转移非法资产

image-20231224105100083

攻击者先通过Monoswap查看池子中USDC的价格和余额,随后通过uniswap的USDC/WETH池接入WETH,乐观转账,在uniswapV2call()函数中调用Monoswap的swapTokenForExactToken()函数,将价格极高的Mono代币,换成一定数量的USDB,用以偿还uniswap闪电贷中的USDC(在uniswap闪电贷中,其可以通过还对应的pair代币),这样就将高价格的Mono代币转换成了对应的WETH(可以注意一下phalcon上这里的USDC数字,应该只是6位小数)。

随后的资产转移方式相同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/268479.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript常用技巧专题五

文章目录 一、使用适当的命名和注释来提高代码可读性二、优雅的写条件判断代码2.1、普通的if else2.2、三元运算符2.3、多个if else2.4、switch case2.5、对象写法2.6、Map写法 三、封装条件语句四、函数应该只做一件事五、Object.assign给默认对象赋默认值六、函数参数两个以下…

OpenGL 绘制Mesh数据(Qt)

文章目录 一、简介二、实现代码三、实现效果一、简介 Mesh数据的结构主要就是点与三角面片,因此本质上仍然是对三角面片进行绘制。这里我们借助VCG这个库实现对Mesh数据的读取,这个库相对简单轻巧,很方便使用。 二、实现代码 由于修改的部分很多,我们逐一进行解释一下: --…

云服务器安装Docker并启动相关服务

云服务器安装Docker 环境准备1、云服务器2、在windterm创建会话2.1、登录2.2、身份验证失败的解决方案在创建好终端服务器修改密码修改会话设置取消不必要的验证 2.3生成密钥流程 3、安装docker3.1 安装报错3.2 解决方案3.2.1操作步骤3.2.1.1 查看当前目录下有那些文件3.2.1.2跳…

智能算法(GA、DBO等)求解零空闲流水车间调度问题(NIFSP)

先做一个声明&#xff1a;文章是由我的个人公众号中的推送直接复制粘贴而来&#xff0c;因此对智能优化算法感兴趣的朋友&#xff0c;可关注我的个人公众号&#xff1a;启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法&#xff0c;经典的&#xff0c;或者是近几年…

6G未来的潜在应用场景

虽然目前6G还不是一种可行技术&#xff0c;但是离6G技术成熟和普及的时间应该不远了。未来6G一旦普及&#xff0c;将能够支持全球更大的设备网络&#xff0c;彻底改变医疗保健等行业的应用&#xff0c;并将有助于更多技术的开发和普及。 虽然过渡到6G技术需要时间&#xff0c;…

【C++11/17】std::map高效插入

我们在使用stl的映射容器std::map时&#xff0c;经常需要向容器中插入数据。由于map的元素key值是唯一的&#xff0c;我们经常遇到这样的场景&#xff1a; 向map中插入元素时&#xff0c;指定的key已经存在则直接更新&#xff1b;指定的key不存在&#xff0c;然后才做插入操作…

解读SPP / SPPF / SimSPPF / ASPP / RFB / SPPCSPC

SPP与SPPF 一、SPP的应用的背景 在卷积神经网络中我们经常看到固定输入的设计&#xff0c;但是如果我们输入的不能是固定尺寸的该怎么办呢&#xff1f; 通常来说&#xff0c;我们有以下几种方法&#xff1a; &#xff08;1&#xff09;对输入进行resize操作&#xff0c;让他们…

Netty-4-网络编程模式

我们经常听到各种各样的概念——阻塞、非阻塞、同步、异步&#xff0c;这些概念都与我们采用的网络编程模式有关。 例如&#xff0c;如果采用BIO网络编程模式&#xff0c;那么程序就具有阻塞、同步等特质。 诸如此类&#xff0c;不同的网络编程模式具有不同的特点&#xff0c…

linux循环调度执行

9.2 循环调度执行 9.2.1 简介 cron的概念和crontab是不可分割的。 ​ crontab是一个命令&#xff0c;常见于Unix和Linux的操作系统之中用于设置周期性被执行的指令。 ​ 该命令从标准输入设备读取指令&#xff0c;并将其存放于“crontab”文件中&#xff0c;以供之后读取和执…

文章标题(备注)

现在也裁员了吗&#xff1f;怎么感觉越来越垃圾 这个又是什么&#xff1f;真搞笑&#xff0c;我也没开隐私呀

Linux:jumpserver介绍(1)

官方网站 JumpServer - 开源堡垒机 - 官网https://www.jumpserver.org/ JumpServer 是广受欢迎的开源堡垒机&#xff0c;是符合 4A 规范的专业运维安全审计系统。JumpServer 帮助企业以更安全的方式管控和登录所有类型的资产&#xff0c;实现事前授权、事中监察、事后审计&…

本地搜索文件太慢怎么办?用Everything搜索秒出结果(附安装包)

每次用电脑本地的搜索都慢的一批&#xff0c;后来发现了一个搜索利器 基本上搜索任何文件都不用等待。 并且页面非常简洁&#xff0c;也没有任何广告&#xff0c;用起来非常舒服。 软件官网如下&#xff1a; voidtools 官网提供三个版本&#xff0c;用起来差别不大。 网盘链…

复分析——第1章——复分析准备知识(E.M. Stein R. Shakarchi)

第一章 复分析准备知识 (Preliminaries to Complex Analysis) The sweeping development of mathematics during the last two centuries is due in large part to the introduction of complex numbers; paradoxically, this is based on the seemingly absurd no…

Shell三剑客:awk(awk编辑编程)一

一、awk脚本定义格式 格式1&#xff1a; BEGIN{} pattern{} END{}格式2&#xff1a; #!/bin/awk -f #add x right BEGIN{} pattern{} END{} BEGIN{ 这里面放的是执行前的语句 }END {这里面放的是处理完所有的行后要执行的语句 }{这里面放的是处理每一行时要执行的语句}格式1假…

整数规划-割平面法

整数规划-割平面法 割平面法思想Gomorys割平面法原理实例 谨以此博客作为学习期间的记录。 割平面法思想 在之前&#xff0c;梳理了分支定界法的流程:分支定界法 除了分支定界法&#xff0c;割平面法也是求解整数规划的另一个利器。 我们已经知道&#xff0c;线性规划的可行域…

智能优化算法应用:基于广义正态分布算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于广义正态分布算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于广义正态分布算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.广义正态分布算法4.实验参数设定…

Grafana二进制部署并配置prometheus数据源

1、获取grafna二进制安装包 https://grafana.com/grafana/download?pggraf&plcmtdeploy-box-1 grafana官网下载地址 [rootambari-hadoop1 ~]# cd /opt/module/grafana/ [rootambari-hadoop1 grafana]# pwd /opt/module/grafana2、在安装自己的安装目录执行 wget https:…

国漫风向标!2023年玄机科技斩获6项腾讯金鹅荣誉

12月16日&#xff0c;2023腾讯视频金鹅荣誉发布&#xff0c;玄机科技凭借其卓越的制作实力和市场认可度&#xff0c;斩获了6项大奖&#xff01;这一荣誉的背后&#xff0c;是玄机科技无数次的创新与突破&#xff0c;也是对其不懈努力的肯定与鼓励。 玄机科技一直以其精良的制作…

半导体晶圆制造SAP:助力推动新时代科技创新

随着科技的迅猛发展&#xff0c;半导体行业成为了推动各行各业进步的重要力量。而半导体晶圆制造作为半导体产业链的核心环节&#xff0c;其效率和质量的提升对于整个行业的发展起着决定性的作用。在这个高度竞争的行业中&#xff0c;如何提升制造过程的效率、降低成本&#xf…

显示器屏幕oled的性能、使用场景、维护

OLED显示器屏幕具有许多独特的性能和使用场景&#xff0c;以下是关于OLED显示器屏幕的性能、使用场景和维护的详细介绍&#xff1a; 一、性能 色彩鲜艳&#xff1a;OLED显示器屏幕能够呈现出更加鲜艳的色彩&#xff0c;色彩饱和度高&#xff0c;色彩还原性好&#xff0c;可以给…