Closed sherlock-admin closed 9 months ago
I don't feel this is a valid issue.
In order for the wETH asset to use the UniV3 pool as a price feed source, it needs to resolve the price of OHM using getPrice()
. However, as that is the second step in the test case, PRICE will revert with an error that OHM is not an approved asset. When I ran the test case, I received that exact error:
[FAIL. Reason: PRICE_PriceFeedCallFailed(0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3)] test_RecursiveUniV3() (gas: 1112413)
Traces:
[1112413] PriceV2Test::test_RecursiveUniV3()
├─ [0] VM::startPrank(ModuleTestFixture: [0x117C65b3995f51936afB66fa3c66296D2B354915])
│ └─ ← ()
├─ [2997] OlympusPricev2::getSubmoduleForKeycode(0x50524943452e434841494e4c494e4b0000000000000000000000000000000000) [staticcall]
│ └─ ← ChainlinkPriceFeeds: [0x99e37f6D9F7Af71351A60b1cDD916Ee472a931ec]
├─ [2352] OlympusPricev2::decimals() [staticcall]
│ └─ ← 18
├─ [15545] ChainlinkPriceFeeds::getOneFeedPrice(MockERC20: [0xdFA2Bb34a233298336fE0bF10fADda4E4603e9cD], 18, 0x000000000000000000000000806a39dd29d6c8cf4bf35963a075eb7ae4eb7b700000000000000000000000000000000000000000000000000000000000015180) [staticcall]
│ ├─ [2340] MockPriceFeed::decimals() [staticcall]
│ │ └─ ← 8
│ ├─ [7083] MockPriceFeed::latestRoundData() [staticcall]
│ │ └─ ← 1, 200000000000 [2e11], 0, 1608336000 [1.608e9], 1
│ └─ ← 2000000000000000000000 [2e21]
├─ [1045771] OlympusPricev2::addAsset(MockERC20: [0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3], true, false, 432000 [4.32e5], 1608336000 [1.608e9], [1855846295220781833263 [1.855e21], 1951878728671415474615 [1.951e21], 1965440266510336798525 [1.965e21], 1960147867268711278075 [1.96e21], 2111090864048154311336 [2.111e21], 2023668389616712338321 [2.023e21], 1986325470766305789479 [1.986e21], 1856205467494912428259 [1.856e21], 1786702731249314109404 [1.786e21], 1696773723883489182720 [1.696e21], 1804310637902476800000 [1.804e21], 2003009145096000000000 [2.003e21], 2103117540000000000000 [2.103e21], 1991400000000000000000 [1.991e21], 2000000000000000000000 [2e21]], Component({ target: 0x50524943452e53494d504c455354524154454759000000000000000000000000, selector: 0xe309de0d00000000000000000000000000000000000000000000000000000000, params: 0x000000000000000000000000000000000000000000000000000000000000012c }), [Component({ target: 0x50524943452e434841494e4c494e4b0000000000000000000000000000000000, selector: 0x8daf88ac00000000000000000000000000000000000000000000000000000000, params: 0x000000000000000000000000806a39dd29d6c8cf4bf35963a075eb7ae4eb7b700000000000000000000000000000000000000000000000000000000000015180 }), Component({ target: 0x50524943452e554e495633000000000000000000000000000000000000000000, selector: 0x8807610b00000000000000000000000000000000000000000000000000000000, params: 0x000000000000000000000000dacaec5fa996be92f45ef16840a1ba8b85c85a68000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000000 })])
│ ├─ [3049] Kernel::modulePermissions(0x5052494345000000000000000000000000000000000000000000000000000000, ModuleTestFixture: [0x117C65b3995f51936afB66fa3c66296D2B354915], 0xcc79ed6500000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ └─ ← true
│ ├─ emit PriceStored(asset_: MockERC20: [0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3], price_: 2000000000000000000000 [2e21], timestamp_: 1608336000 [1.608e9])
│ ├─ [5045] ChainlinkPriceFeeds::getOneFeedPrice(MockERC20: [0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3], 18, 0x000000000000000000000000806a39dd29d6c8cf4bf35963a075eb7ae4eb7b700000000000000000000000000000000000000000000000000000000000015180) [staticcall]
│ │ ├─ [340] MockPriceFeed::decimals() [staticcall]
│ │ │ └─ ← 8
│ │ ├─ [1083] MockPriceFeed::latestRoundData() [staticcall]
│ │ │ └─ ← 1, 200000000000 [2e11], 0, 1608336000 [1.608e9], 1
│ │ └─ ← 2000000000000000000000 [2e21]
│ ├─ [40057] UniswapV3Price::getTokenTWAP(MockERC20: [0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3], 18, 0x000000000000000000000000dacaec5fa996be92f45ef16840a1ba8b85c85a68000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ ├─ [4817] MockUniV3Pair::slot0() [staticcall]
│ │ │ └─ ← 0, 0, 0, 0, 0, 0, true
│ │ ├─ [2287] MockUniV3Pair::token0() [staticcall]
│ │ │ └─ ← MockERC20: [0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3]
│ │ ├─ [2936] MockUniV3Pair::token1() [staticcall]
│ │ │ └─ ← MockERC20: [0x2b42C737b072481672Bb458260e9b59CB2268dc6]
│ │ ├─ [270] MockERC20::decimals() [staticcall]
│ │ │ └─ ← 9
│ │ ├─ [270] MockERC20::decimals() [staticcall]
│ │ │ └─ ← 18
│ │ ├─ [11985] UniswapV3OracleHelper::getTWAPRatio(MockUniV3Pair: [0xDACAEc5FA996be92F45Ef16840A1Ba8B85c85A68], 60, MockERC20: [0x25690f2BCf09D7e1286Fc35378433CfD3006E7C3], MockERC20: [0x2b42C737b072481672Bb458260e9b59CB2268dc6], 18) [delegatecall]
│ │ │ ├─ [6410] MockUniV3Pair::observe([60, 0]) [staticcall]
│ │ │ │ └─ ← [-100000000 [-1e8], -109255432 [-1.092e8]], []
│ │ │ └─ ← 199983942441 [1.999e11]
│ │ ├─ [2781] OlympusPricev2::getPrice(MockERC20: [0x2b42C737b072481672Bb458260e9b59CB2268dc6], 0) [staticcall]
│ │ │ └─ ← PRICE_AssetNotApproved(0x2b42C737b072481672Bb458260e9b59CB2268dc6)
│ │ └─ ← PRICE_AssetNotApproved(0x2b42C737b072481672Bb458260e9b59CB2268dc6)
This is by design - when adding an asset, the configuration is validated so that an invalid asset configuration cannot be added.
Krishnakumarskr
medium
OlympusPricev2::getPrice()
with Variant.Current might run out of gas on using UniV3 and BPT price feedsSummary
When
OlympusPricev2::getPrice()
is called with Variant.Current for a certain asset whose UniV3 or Balancer price feed was added with selectorUniswapV3Price.getTokenTWAP.selector
orBalancerPoolTokenPrice.getTokenPriceFromWeightedPool.selector
it recursively calls the getPrice() function resulting in the utilization of millions in gas.Vulnerability Detail
For this issue, the following scenario must apply:
Uniswap V3 price feed When getPrice() is called for an asset OHM or WETH with variant as Current, the getPrice() will be called recursively from
UniswapV3Price::getTokenTWAP()
to search for a quote token.Steps:
OlympusPricev2::getPrice(address(ohm), Variant.Current)
OlympusPricev2
callsUniswapV3Price::getTokenTWAP()
with OHM as a lookUpToken.UniswapV3Price::getTokenTWAP()
callsOlympusPricev2::getPrice(address(weth), Variant. Current)
with WETH as the quote token.OlympusPricev2
callsUniswapV3Price::getTokenTWAP()
with WETH as a lookUpToken.UniswapV3Price::getTokenTWAP()
callsOlympusPricev2::getPrice(address(ohm), Variant. Current)
with OHM as the quote token.Balancer price feed When getPrice() is called for an asset OHM or RSV with a variant as Current, the getPrice() will be called recursively from
BalancerPoolTokenPrice::getTokenPriceFromWeightedPool()
to search for a destination token. Steps:OlympusPricev2::getPrice(address(ohm), Variant.Current)
OlympusPricev2
callsBalancerPoolTokenPrice::getTokenPriceFromWeightedPool()
with OHM as a lookUpToken.BalancerPoolTokenPrice::getTokenPriceFromWeightedPool()
callsOlympusPricev2::getPrice(address(reserve), Variant. Current)
with RSV as the destination token.OlympusPricev2
callsBalancerPoolTokenPrice::getTokenPriceFromWeightedPool()
with RSV as a lookUpToken.BalancerPoolTokenPrice::getTokenPriceFromWeightedPool()
callsOlympusPricev2::getPrice(address(ohm), Variant. Current)
with OHM as the destination token.Surprisingly, for some reason this recursive call is finite and the final price is being returned, but still, from the POC below you can see it made around 120 calls to getPrice() utilizing a maximum of 6+ million in gas.
POC for UniV3
The below function can be pasted inside
PRICE.v2.t.sol
Note: Running the test with the following command where the gas limit is set to
50 Million
Logs from the above test
From the log, the total gas used is 6004357 which is around
6 million
in gasgetPrice()
was called 118 times and the max gas utilized is6 Million
POC for BPT
The below function can be pasted inside
PRICE.v2.t.sol
Note: Running the test with the following command where the gas limit is set to
50 Million
Logs from the above test
From the log, the total gas used is 6351887 which is around
6.3 million
in gasgetPrice()
was called 120 times and the max gas utilized is6.3 Million
The same issue will arise while adding the BPT token of the OHM/RSV pool from
OlympusPricev2::addAsset()
since this function calls the_getCurrentPrice()
function. The impact is less here since it will only be called by the protocol and not public.This issue will also occur if the price feed is added with the selector
BalancerPoolTokenPrice.getTokenPriceFromStablePool.selector
.Impact
Any internal or external contract using the
getPrice()
function has to send extremely high gas for the call to be successful. Any protocol's contract or external contract using this function will be affected. If a default estimated gas were sent then most of the time the transaction would run out of gas. Under the scenario mentioned above, the getPrice() function could be unusable because of high gas utilization.Code Snippet
Call to submodules from Olympusv2 https://github.com/sherlock-audit/2023-11-olympus/blob/9c8df76dc9820b4c6605d2e1e6d87dcfa9e50070/bophades/src/modules/PRICE/OlympusPrice.v2.sol#L143
Call to Olympusv2::getPrice() from
getTokenTWAP()
https://github.com/sherlock-audit/2023-11-olympus/blob/9c8df76dc9820b4c6605d2e1e6d87dcfa9e50070/bophades/src/modules/PRICE/submodules/feeds/UniswapV3Price.sol#L165Call to Olympusv2::getPrice() from
getTokenPriceFromWeightedPool()
https://github.com/sherlock-audit/2023-11-olympus/blob/9c8df76dc9820b4c6605d2e1e6d87dcfa9e50070/bophades/src/modules/PRICE/submodules/feeds/BalancerPoolTokenPrice.sol#L658Tool used
Manual Review
Recommendation
Mitigating this issue is a little tricky to do without state storage. One possible solution is to skip looking for a price feed if
msg. sender
is from the same Module.