根据应用修改的竞赛题目-uniswap篇

admin 2022年12月6日17:29:38评论32 views字数 9318阅读31分3秒阅读模式
根据应用修改的竞赛题目-uniswap篇
一:以太坊应用

根据应用修改的竞赛题目-uniswap篇
在2018年之后,以太坊的实际应用开始落地,如uniswap这类AMM,还有DAO这种去中心化自治组织。而现实中的安全事件也大多为对以太坊应用的攻击。

现在,对以太坊应用的攻击相关的题目也出现在竞赛中。
根据应用修改的竞赛题目-uniswap篇
二:Uniswap 简介

根据应用修改的竞赛题目-uniswap篇
Uniswap是一种AMM(自动化做市商),它实现了一个去中心化的交易所,用户可以使用代币A去对换代币B,而这所有过程都由合约实现。


Uniswap会对每两个代币组成的交易对生成一个交易池,池子中存放两种代币。向池子中同时增加两种代币的行为被称作添加流动性,撤回池子中的两种代币被称为减少流动性。向池子发送代币A会收到代币B,这种行为被称为对换(swap),对换过程中,池子中的两种代币数量x,y满足:


其中k是一个定值,而如果用x换y,则满足:


换出的y为:


如果交易量对于池子的总量比较少的话,使用该池子对换x的价格应该接近于x与y的比值。

其对换的具体实现在uniswapV2Pair.sol中的uniswapV2pair合约的swap函数中:
// 交易token,需要输入两种token的量,token要发送到的地址,data用于闪电贷的回调
    // this low-level function should be called from a contract which performs important safety checks
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
      // 确保至少一个数量大于0
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        // 获取两种代币的储备量
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        // 确保要有足够的余额
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        // 发送地址不能为这两个token合约
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        
        // 发送代币
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        
        // 如果data有长度,则调用to的接口进行闪电贷
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        
        // 获取合约中两种代币的余额
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        }
        
    // amountIn = balance - (_reserve - amountOut)
        // 根据取出的储备量、原有的储备量及最新的余额,反求出输入的金额
        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        // 确保输入的金额至少有一个大于0
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        
        // 调整后的余额 = (1 - 0.3%)* 原余额
        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
        
        // 新k值应该大于旧的k值,增加值为手续费
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
        }
    
    // 更新储备量
        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

同时,uniswap也提供预言机功能。因为在一个池子中的两种代币的相对价格就是两种代币数量的比值,所以其他链上应用想要获取某两种代币的相对价格,可以到uniswap的池子中获取。所以攻击者可以通过操纵池子中代币的数量来控制价格,炒高或炒低某种代币的价格,并再使用其他链上应用获取收益。
根据应用修改的竞赛题目-uniswap篇
三:实例

根据应用修改的竞赛题目-uniswap篇
contract ChaitinBank {
    using SafeMath for uint256;
    address public feicoin;
    address public owner;
    IERC20 public flagToken;
    address public chaitinFeifeiTokenPair;
    address public chaitinSwap;
    uint256 public CTTperFlag;

    constructor(address _feicoin, address _owner, address _flagToken, address _chaitinFeifeiTokenPair, address _chaitinSwap, uint256 _CTTperFlag) public {
        feicoin = _feicoin;
        flagToken = IERC20(_flagToken);
        owner = _owner;
        chaitinFeifeiTokenPair = _chaitinFeifeiTokenPair;
        CTTperFlag = _CTTperFlag;
        chaitinSwap = _chaitinSwap;
    }

    // 计算feicoin与chaitin的相对价格 
    function calcFeiCoinValue() public view returns(uint256 value){
        (uint256 reserve0, uint256 reserve1, uint32 time ) = IPokePair(chaitinFeifeiTokenPair).getReserves(); //TODO:params
        address token0 = IPokePair(chaitinFeifeiTokenPair).token0();
        if(token0 == feicoin){
            value = IPokeRouter01(chaitinSwap).getAmountOut( 10, reserve0, reserve1);
        }else{
            value = IPokeRouter01(chaitinSwap).getAmountOut( 10, reserve1, reserve0);
        }

        return value;
    }

    // Enter the bank. Pay some feifeicoin. Earn some Flags. 用feicoin换flagcoin
    function depositFeiCointoFlag(uint256 _amount) public {
        // 用户向本合约发送feicoin
        IERC20(feicoin).transferFrom(msg.sender, address(this), _amount);
        // 计算feicoin与flagcoin的相对价格 
        uint256 valueOfFeicoin = calcFeiCoinValue(); // exchange feicoin to ChaitinCoin
        uint256 exceptFlagsAmount = valueOfFeicoin * ( _amount / 10 ) / CTTperFlag;

        uint256 flagsAmount = 0;
        if(exceptFlagsAmount <= flagToken.balanceOf(address(this))){
            flagsAmount = exceptFlagsAmount;
        }else{
            flagsAmount = flagToken.balanceOf(address(this));
        }
        // 向用户发flagtoken
        flagToken.transfer(msg.sender, flagsAmount);

    }

    // Leave the Flags. Claim back your FeiCoins. 用flagcoin换feicoin
    function leaveFlagstoFeiCoin(uint256 _amount) public {  //TODO: how to define
        // 向本合约发送flagcoin
        flagToken.transferFrom(msg.sender, address(this), _amount);
        uint256 valueOfFeicoin = calcFeiCoinValue();
        uint256 exceptFeicoinAmount = _amount * CTTperFlag / valueOfFeicoin;

        uint256 feicoinsAmount = 0;
        if (exceptFeicoinAmount <= IERC20(feicoin).balanceOf(address(this))){
            feicoinsAmount = exceptFeicoinAmount;
        }else{
            feicoinsAmount = IERC20(feicoin).balanceOf(address(this));
        }

        IERC20(feicoin).transfer(msg.sender, feicoinsAmount);
    }
}
这个题目获取flag的条件是要拥有80ether的flag coin,而想要获取flag coin则在ChaitinBank合约的

depositFeiCointoFlag函数中,该函数可以用fei coin去换flagcoin。

用户首先要向该合约发送一定的feicoin,并调用calcFeiCoinValue函数计算feitoken和chaintin token的相对价格,这个函数的具体实现就是调用的uniswap的预言机功能。在获取完价格后,合约会给用户发送相应数量的flag coin。

但是题目一开始只给我们提供了1wei的Chaitin token,所以必须要先到pair合约换到足够的fei coin。

Pair合约的swap函数为:
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata dataexternal lock {
        require(amount0Out > 0 || amount1Out > 0'Chaitin: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Chaitin: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'Chaitin: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        if (data.length > 0) IChaitinCallee(to).ChaitinCall(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        }
        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0'Chaitin: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        uint balance0Adjusted = balance0.mul(10000).sub(amount0In.mul(25));
        uint balance1Adjusted = balance1.mul(10000).sub(amount1In.mul(25));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'Chaitin: K');
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }
在这个函数中判断k值是否合法时,将k值缩小为了原来的1%:
uint balance0Adjusted = balance0.mul(10000).sub(amount0In.mul(25));
        uint balance1Adjusted = balance1.mul(10000).sub(amount1In.mul(25));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_rserve0).mul(_reserve1).mul(1000**2), 'Chaitin: K');

这将导致什么后果呢?正常情况下,对换之前应满足:


对换之后应满足:


但由于k值变为了原来的1%,现在只需满足:

换出来的y为:


同样换出的x为:
可以发现从池子中直接取走大部分的两种代币,我们先向pair合约发送1wei的chaiting token,接着调用swap函数,这里设置amount0Out和amount1Out都为90Ether,address地址为自己的地址,字段data为 0x。这时看我们的两种token的余额,都达到了90Ether。

这时再回到Bank合约去换flag coin,但是feitoken与flagtoken的相对价格和feitoken对chaitintoken价格有关,而且目前feitoken的数量不足够换到80ether的flag token。所以这时候需要通过预言机攻击来抬高feitoken的价格。

在Router和余额的swapTokensForExactTokens函数可以直接对换token:
function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    
external virtual override ensure(deadlinereturns (uint[] memory amounts
{
        amounts = ChaitinLibrary.getAmountsIn(factory, amountOut, path);
        require(amounts[0] <= amountInMax, 'ChaitinRouter: EXCESSIVE_INPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, ChaitinLibrary.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
调用这个函数买入大量的feitoken,这时池子中的feitoken数量减少,chaitintoken数量增加,feitoken价格升高,这时使用升值的feitoken买入flagtoken,理论上能获取100ether,获取flag成功。
根据应用修改的竞赛题目-uniswap篇
四:总结

根据应用修改的竞赛题目-uniswap篇
这道题没有对solidity和EVM本身存在的漏洞进行攻击,一个是针对k值计算的逻辑,k值是整个uniswap的核心,如果k值的计算出现错误,攻击者可能会直接掏空整个池子的价值。另外一个是对预言机功能的攻击,这种攻击可以让攻击者以一个比较小的代价操纵价格,并用异常的价格获利。

要解决这种与应用有关的题目,要对这些写项目的原理和实现非常熟悉。
       

原文始发于微信公众号(山石网科安全技术研究院):根据应用修改的竞赛题目-uniswap篇

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年12月6日17:29:38
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   根据应用修改的竞赛题目-uniswap篇https://cn-sec.com/archives/1448017.html

发表评论

匿名网友 填写信息