sherlock-2024-01-ubiquity-m01

[M-01] LibUbiquityPool::mintDollar/redeemDollar reliance on outdated TWAP oracle may be inefficient for preventing depeg

보고서

Summary

uAD를 발행/소각하기 위해 토큰 가격을 Curve TWAP 오라클에서 얻는다. 이 때, TWAP 오라클은 오래된(outdated) 상태일 수 있다. 왜냐하면 Ubiquity 풀을 호출할 때, 기반하는 metapool을 최신화하지 않고 사용하기 때문이다. 메타풀의 가격 정보는 유동성을 공급했거나 스왑을 하는 등의 인터렉션이 발생했을 때 업데이트 된다. 즉, 인터렉션이 없었다면 가격이 오랫동안 업데이트되지 않을 수 있다.

Keyword

curve oracle, price oracle, staled oracle

Vulnerability

uAD를 발행 또는 소각할 때 먼저 가격 정보를 최신 상태로 유지하는 LibTWAPOracle.update 함수를 호출한다.

    function mintDollar(
        uint256 collateralIndex,
        uint256 dollarAmount,
        uint256 dollarOutMin,
        uint256 maxCollateralIn
    )
        internal
        collateralEnabled(collateralIndex)
        returns (uint256 totalDollarMint, uint256 collateralNeeded)
    {
        ...
        // update Dollar price from Curve's Dollar Metapool
@>      LibTWAPOracle.update();
        
        ...
        ubiquityDollarToken.mint(msg.sender, totalDollarMint);
    }
 
    function redeemDollar(
        uint256 collateralIndex,
        uint256 dollarAmount,
        uint256 collateralOutMin
    )
        internal
        collateralEnabled(collateralIndex)
        returns (uint256 collateralOut)
    {
        ...
 
        // update Dollar price from Curve's Dollar Metapool
@>      LibTWAPOracle.update();
        
        ...
@>      ubiquityDollarToken.burnFrom(msg.sender, dollarAmount);
    }

LibTWAPOracle.update 에서는 기반하는 메타풀의 update 함수를 호출하지 않고 메타풀의 가격 정보를 조회한다. 따라서 메타풀이 리턴하는 가격 정보는 오래되었을 수 있다.

    function update() internal {
        TWAPOracleStorage storage ts = twapOracleStorage();
        (
            uint256[2] memory priceCumulative,
            uint256 blockTimestamp
@>      ) = currentCumulativePrices(); // @audit-info 메타풀에서 최신 가격 정보를 가져옴
        if (blockTimestamp - ts.pricesBlockTimestampLast > 0) {
            // get the balances between now and the last price cumulative snapshot
            uint256[2] memory twapBalances = IMetaPool(ts.pool)
                .get_twap_balances(
                    ts.priceCumulativeLast,
                    priceCumulative,
                    blockTimestamp - ts.pricesBlockTimestampLast
                );
 
            // price to exchange amountIn Ubiquity Dollar to 3CRV based on TWAP
            ts.price0Average = IMetaPool(ts.pool).get_dy(
                0,
                1,
                1 ether,
                twapBalances
            );
 
            // price to exchange amountIn 3CRV to Ubiquity Dollar based on TWAP
            ts.price1Average = IMetaPool(ts.pool).get_dy(
                1,
                0,
                1 ether,
                twapBalances
            );
            // we update the priceCumulative
            ts.priceCumulativeLast = priceCumulative;
            ts.pricesBlockTimestampLast = blockTimestamp;
        }
    }
 
    function currentCumulativePrices()
        internal
        view
        returns (uint256[2] memory priceCumulative, uint256 blockTimestamp)
    {
        address metapool = twapOracleStorage().pool;
@>      priceCumulative = IMetaPool(metapool).get_price_cumulative_last(); // @audit-info 메타풀에서 데이터 가져옴 (최신 상태가 아닐 수 있음)
@>      blockTimestamp = IMetaPool(metapool).block_timestamp_last();
    }

Impact

악의적인 사용자가 오래된 가격을 이용하여 uAD를 대량으로 발행/소각하면 토큰의 가치를 더 떨어뜨릴 수 있다.

Mitigation

메타풀의 가격 정보는 유동성을 공급했거나 스왑을 하는 등의 인터렉션이 발생했을 때 업데이트 된다. 즉, 인터렉션이 없었다면 가격이 오랫동안 업데이트되지 않을 수 있다. 가격을 조회하기 전, 메타풀에 remove_liquidity 를 파라미터 0과 함께 호출하여 가격을 최신화시킨다.

def remove_liquidity(
    _burn_amount: uint256,
    _min_amounts: uint256[N_COINS],
    _receiver: address = msg.sender
)

tags: bughunting, ubiquity, smart contract, solidity, curve oracle, price oracle, staled oracle, severity medium