Balancer 漏洞分析

  • Archime
  • 更新于 2022-11-02 17:26
  • 阅读 4132

Balancer 漏洞分析

1、Balancer 漏洞简介

https://medium.com/balancer-protocol/incident-with-non-standard-erc20-deflationary-tokens-95a0f6d46dea

1.png

2、攻击分析

交易:https://phalcon.blocksec.com/tx/eth/0x013be97768b702fe8eccef1a40544d5ecb3c1961ad5f87fee4d16fdc08c78106

2.png

3、获利分析

3.png

4、攻击思想

对于使用恒定乘积公式的AMM的价格操纵获利,一种常见的方法是使得交易池币对中有一方数量失衡最终导致价格失衡,用极少量的代币A即可兑换出大量代币B。 通常具体的攻击步骤为:

  1. 攻击者一般通过闪电贷获取资金,在闪电贷中回调攻击者的漏洞利用函数;
  2. 攻击者使用从闪电贷中获取的资金,投入存在漏洞的AMM合约(此处为 Balancer Pool Token , BPT )),兑换出大量的代币(在此次Banalancer事件中为STA代币),最终目的是为了在完成攻击后重新兑换出高价值代币;
  3. 攻击者攻击漏洞合约,实现代币数量操纵而导致价格失衡;
  4. 攻击者重新在被攻击后的交易所中投入之前兑换出的代币,因为此时价格已失衡,攻击者可以使用极少的代币A即可获得高价值代币B;
  5. 攻击者套利后,归还闪电贷,离场;

    5、攻击过程&漏洞原因

    根据以上攻击思路,从交易记录分析攻击过程:

  6. 攻击者先调用WETH、STA等代币的approve函数为之后的还款、攻击等代币转移操作做准备。攻击者通过dYdX交易所获取启动资金WETH,这是为了之后投入BPT交易所兑换出STA代币。dYdX合约将调用攻击者的回调函数callFunction ,攻击者将在该函数中实现攻击逻辑;

4.png

  1. 攻击者将从闪电贷中获取的资金反复投入BPT交易所中,从而兑换出大量的STA,最终导致BPT中的STA代币数量为1,而WETH数量约为60695 ,此时STA/WETH 的价格极高(最初几次兑换不知是否故布疑云)。

5.png

6.png

  1. 如果此时攻击者必须想办法将BPT 交易所中的STA数量始终保持在极小值,否则随着攻击者的重新用STA兑换WETH 将导致STA/WETH 的价格越来越低。从交易记录可以看出,攻击者每次调用了 swapExactAmountIn 后,紧接着调用了 gulp 函数,需要注意的是攻击者每次兑换STA的数量为1;

7.png swapExactAmountIn 函数的业务逻辑见代码注释:

function swapExactAmountIn(
        address tokenIn,
        uint tokenAmountIn,
        address tokenOut,
        uint minAmountOut,
        uint maxPrice
    )
        external
        _logs_
        _lock_
        returns (uint tokenAmountOut, uint spotPriceAfter)
    {

        require(_records[tokenIn].bound, "ERR_NOT_BOUND");
        require(_records[tokenOut].bound, "ERR_NOT_BOUND");
        require(_publicSwap, "ERR_SWAP_NOT_PUBLIC");

        //1、获取转帐前的两种代币数量记录
        Record storage inRecord = _records[address(tokenIn)];
        Record storage outRecord = _records[address(tokenOut)];

        require(tokenAmountIn <= bmul(inRecord.balance, MAX_IN_RATIO), "ERR_MAX_IN_RATIO");

        //2、获取交易前的价格
        uint spotPriceBefore = calcSpotPrice(
                                    inRecord.balance,
                                    inRecord.denorm,
                                    outRecord.balance,
                                    outRecord.denorm,
                                    _swapFee
                                );
        //3、判断交易价格是否小于用户设置的maxPrice,用于防止三明治攻击,即当价格滑点大于用户的预期最大可接受值,将终止交易
        require(spotPriceBefore <= maxPrice, "ERR_BAD_LIMIT_PRICE");

        tokenAmountOut = calcOutGivenIn(
                            inRecord.balance,
                            inRecord.denorm,
                            outRecord.balance,
                            outRecord.denorm,
                            tokenAmountIn,
                            _swapFee
                        );
        //4、此处判断兑换出的代币数量是否大于用户的设置的minAmountOut,可用于防止三明治攻击
        require(tokenAmountOut >= minAmountOut, "ERR_LIMIT_OUT");

        //5、此处是漏洞利用关键,BPT 合约直接将用户的输入代币数量记录在总代币数量中
        inRecord.balance = badd(inRecord.balance, tokenAmountIn);
        outRecord.balance = bsub(outRecord.balance, tokenAmountOut);

        //6、BPT 合约在完成转账后重新计算价格,校验价格是否在预期范围内,防止价格被操纵
        spotPriceAfter = calcSpotPrice(
                                inRecord.balance,
                                inRecord.denorm,
                                outRecord.balance,
                                outRecord.denorm,
                                _swapFee
                            );
        require(spotPriceAfter >= spotPriceBefore, "ERR_MATH_APPROX");     
        require(spotPriceAfter <= maxPrice, "ERR_LIMIT_PRICE");
        require(spotPriceBefore <= bdiv(tokenAmountIn, tokenAmountOut), "ERR_MATH_APPROX");

        emit LOG_SWAP(msg.sender, tokenIn, tokenOut, tokenAmountIn, tokenAmountOut);
        //7、调用 STA、WETH的transferFrom函数转移代币
        _pullUnderlying(tokenIn, msg.sender, tokenAmountIn);
        _pushUnderlying(tokenOut, msg.sender, tokenAmountOut);

        return (tokenAmountOut, spotPriceAfter);
    }

查看_pullUnderlying函数的逻辑,发现其代码逻辑是直接使用相关代币的transferFrom函数实现代币转移,此处tokenIn 指的是STA 代币:

  function _pullUnderlying(address erc20, address from, uint amount)
        internal
    {
        bool xfer = IERC20(erc20).transferFrom(from, address(this), amount);
        require(xfer, "ERR_ERC20_FALSE");
    }

继续跟进 STA 代币的transferFrom函数:

function transferFrom(address from, address to, uint256 value) public returns (bool) {
    require(value <= _balances[from]);
    require(value <= _allowed[from][msg.sender]);
    require(to != address(0));

    _balances[from] = _balances[from].sub(value);

    //因为STA是通缩型代币,每次转移代币将由收款人承担一定的费用,数量为tokensToBurn = cut(value);扣除费用后再将代币转移给接收人
    uint256 tokensToBurn = cut(value);
    uint256 tokensToTransfer = value.sub(tokensToBurn);

    _balances[to] = _balances[to].add(tokensToTransfer);
    _totalSupply = _totalSupply.sub(tokensToBurn);

    _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);

    emit Transfer(from, to, tokensToTransfer);
    emit Transfer(from, address(0), tokensToBurn);

    return true;
  }

CUT函数实现逻辑:

  function cut(uint256 value) public view returns (uint256)  {
    uint256 roundValue = value.ceil(basePercent);
    uint256 cutValue = roundValue.mul(basePercent).div(10000);
    return cutValue;
  }

CUT函数属性为view,直接通过合约查看输入1单位的STA代币将通缩多少,显示结果为1,意味着用户转账的1 STA代币全部燃烧,BPT合约最终没有接收到STA代币: https://etherscan.io/token/0xa7de087329bfcda5639247f96140f9dabe3deed1#readContract

8.png 还剩下一个问题,如果攻击者继续转入STA代币,但BPT合约直接使用合约本地记录的代币数量计算价格,还是无法达到控制价格的目的,此时该使用另一个关键函数gulp :

 Record storage inRecord = _records[address(tokenIn)];
 Record storage outRecord = _records[address(tokenOut)];

在gulp函数中,会使用balanceOf函数重新覆盖BPT记录的值,且这个函数gulp 的可见性为external ,意味着任何人都可调用。根据之前的分析,在STA代币中记录BPT的余额为1,此时会使用1覆盖STA的本地记录,最终使得在BPT交易所中STA/WETH的价格始终保持高位,实现套利。

function gulp(address token)
        external
        _logs_
        _lock_
    {
        require(_records[token].bound, "ERR_NOT_BOUND");
        _records[token].balance = IERC20(token).balanceOf(address(this));
    }

至此,整个攻击逻辑分析完毕。

  1. 归还闪电贷,获利离场:

9.png

  1. 攻击者同样套现LINK、STA、WBTC、SNX等代币。
点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论
Archime
Archime
0x96C4...508C
江湖只有他的大名,没有他的介绍。