sherlock-audit / 2023-12-ubiquity-judging

2 stars 2 forks source link

cducrest-brainbot - TWAPOracle price can be manipulated by depositing / withdrawing from the underlying pool #184

Closed sherlock-admin2 closed 7 months ago

sherlock-admin2 commented 8 months ago

cducrest-brainbot

high

TWAPOracle price can be manipulated by depositing / withdrawing from the underlying pool

Summary

The LibTWAPOracle bases the TWAP price for Ubiquity dollar on a curve meta pool for 3CRV / Ubiquity dollar. The currently deployed version of that pool only holds below 27k$ of 3CRV.

Anyone can manipulate the price of Ubiquity dollar reflected by that pool easily with little investment.

Vulnerability Detail

The function to update the price of Ubiquity dollar and 3CRV calls pool.get_dy() with the twap balances.

        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
        );

get_dy() gets the amount of CRV out for 1 ether of Ubiquity dollar in. get_twap_balances() gets the average balance across two cumulative prices:

def get_twap_balances(_first_balances: uint256[N_COINS], _last_balances: uint256[N_COINS], _time_elapsed: uint256) -> uint256[N_COINS]:
    balances: uint256[N_COINS] = empty(uint256[N_COINS])
    for x in range(N_COINS):
        balances[x] = (_last_balances[x] - _first_balances[x]) / _time_elapsed
    return balances

The cumulative prices are fetched by LibTWAPOracle via currentCumulativePrices():

    function currentCumulativePrices()
        internal
        view
        returns (uint256[2] memory priceCumulative, uint256 blockTimestamp)
    {
        address metapool = twapOracleStorage().pool;
        priceCumulative = IMetaPool(metapool).get_price_cumulative_last();
        blockTimestamp = IMetaPool(metapool).block_timestamp_last();
    }

This calls on the metapool:

def get_price_cumulative_last() -> uint256[N_COINS]:
    return self.price_cumulative_last

The value of price_cumulative_last is updated in:

@internal
def _update():
    """
    Commits pre-change balances for the previous block
    Can be used to compare against current values for flash loan checks
    """
    elapsed_time: uint256 = block.timestamp - self.block_timestamp_last
    if elapsed_time > 0:
        for i in range(N_COINS):
            _balance: uint256 = self.balances[i]
            self.price_cumulative_last[i] += _balance * elapsed_time
            self.previous_balances[i] = _balance
        self.block_timestamp_last = block.timestamp

This purely takes into account the current balance in token of the contract multiplied by the elapsed time. This balance can easily be manipulated by depositing / withdrawing little amounts from the metapool.

Impact

The Ubiquity dollar price in 3CRV (in this protocol taken as the USD price of Ubiquity dollar) can be easily manipulated with very little investment.

This price is used to enforce when minting / redeeming Ubiquity is allowed in LibUbiquityPool.sol and these enforcement no longer hold when price is manipulated.

Code Snippet

https://github.com/sherlock-audit/2023-12-ubiquity/blob/main/ubiquity-dollar/packages/contracts/src/dollar/libraries/LibTWAPOracle.sol#L68-L102

https://github.com/sherlock-audit/2023-12-ubiquity/blob/main/ubiquity-dollar/packages/contracts/src/dollar/libraries/LibTWAPOracle.sol#L129-L137

https://etherscan.io/address/0x20955CB69Ae1515962177D164dfC9522feef567E#code

Tool used

Manual Review

Recommendation

Use a better price oracle

Duplicate of #56