sherlock-2024-01-ubiquity-m03
[M-03] LibUbiquityPool::mintDollar/redeemDollar reliance on arbitrarily short TWAP oracle may be inefficient for preventing depeg
Summary
uAD를 발행/소각하기 위해 토큰 가격을 Curve TWAP 오라클에서 얻는다. 이 때, 너무 짧은 TWAP window를 사용하면 오라클을 조작할 수 있고, 이를 통해 토큰 발행/소각을 방해하거나 발행/소각이 언블록되어야 하는 것을 해제하여 디페깅을 할 수 있다.
Keyword
curve oracle, price oracle, depegging, twap oracle, stablecoin, oracle manipulation
Vulnerability
LibTWAPOracle.consult는 메타풀의 가격을 사용한다.
function consult(address token) internal view returns (uint256 amountOut) {
@> TWAPOracleStorage memory ts = twapOracleStorage();
if (token == LibAppStorage.appStorage().dollarTokenAddress) {
// price to exchange 1 Ubiquity Dollar to 3CRV based on TWAP
@> amountOut = ts.price0Average;
} else {
require(token == ts.token1, "TWAPOracle: INVALID_TOKEN");
// price to exchange 1 3CRV to Ubiquity Dollar based on TWAP
@> amountOut = ts.price1Average;
}
}메타풀 가격은 LibTWAPOracle.update 에서 캐싱한다. 이전 캐싱 이후 ~ 메타풀이 업데이트된 시점을 TWAP window로 사용한다. 이 때 TWAP window가 너무 짧으면 가격을 조작할 수 있다.
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();
}
function update() internal {
TWAPOracleStorage storage ts = twapOracleStorage();
(
uint256[2] memory priceCumulative, // @audit-info [A + 1500 * 12, B + 500 * 12]
uint256 blockTimestamp
) = currentCumulativePrices();
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, // @audit-info [A, B]
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;
}
}다음 예를 살펴보자.
- 블록 N 에서 메타풀 초기 상태
- reserveA: 1000
- reserveB: 1000
- 블록 N+1 에서 메타풀 상태 (+12초)
- reserveA: 1500
- reserveB: 500
메타풀에 상호작용이 일어나면 _update 함수가 호출되어 price_cumulative_last 를 업데이트 한다. 매 블록에서 상호작용이 일어나 업데이트 되었다고 하자.
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.timestampN 블록에서 LibTWAPOracle.update를 호출하고, N+2 블록에서 다시 LibTWAPOracle.update 를 호출, TWAP을 조회했을 때 다음과 같을 것이다.
- ts.priceCumulativeLast: [A, B]
- priceCumulative (
currentCumulativePrices리턴값): [A + 1500 * 12, B + 500 * 12]- 메타풀 업데이트 시
_balance * elapsed_time가 추가되므로, N + 1 블록에서 업데이트되었음
- 메타풀 업데이트 시
- blockTimestamp - ts.pricesBlockTimestampLast = 12
- N 블록에서
LibTWAPOracle.update를 호출했으므로 ts.pricesBlockTimestampLast 는 T, 메타풀은 N + 1 블록에서 업데이트 되었으므로 블록 타임 12초이므로 blockTimestamp는 T + 12초
- N 블록에서
위 값을 파라미터로 get_twap_balances([A, B], [A + 1500 * 12, B + 500 * 12], 12)를 호출하면 [(A + 1500 * 12 - A) / 12, (B + 500 * 12 - B) / 12] = [1500, 500] 으로, 단순히 이전 블록의 메타풀 balance 를 리턴한다.
@view
@external
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 balancesImpact
두 개의 연속된 블록을 제어할 수 있는 악의적인 사용자는 다음 블록의 TWAP을 어떤 상태로든 조작할 수 있다. 이를 통해 다음과 같은 일이 가능하다.
- 다음 블록에서 토큰 발행/소각을 DoS 할 수 있다.
- 다음 블록에서 토큰 발행/소각이 가능하게 언블록하고 uAD를 디페깅할 수 있다.
Mitigation
TWAP window를 충분히 키운다. 15분~30분 사이를 주로 권장하는 것 같다.
function update() internal {
TWAPOracleStorage storage ts = twapOracleStorage();
(
uint256[2] memory priceCumulative,
uint256 blockTimestamp
) = currentCumulativePrices();
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
+ 15 minutes
);
...
}
...
}tags: bughunting, ubiquity, smart contract, solidity, curve oracle, price oracle, depegging, twap oracle, stablecoin, oracle manipulation, severity medium