In the Compatibilities section of the contest, PriceCalculator is declared as an external dependency (and also is used in production).
This contract has multiple issues which could lead to loss of protocol funds.
There are several issues with PriceCalculator:
Protocol could use stale Chainlink prices that could lead to borrowing EURS for non-market prices on a falling market.
DDOS of protocol liquidations in highly fluctuated markets in several networks.
Incorrect average price calculation on change of Chainlink aggregator in Proxy which could lead to liquidations of collateralised vaults.
Please see the description of each below.
Since it is out of scope, they couldn't be sent as separate issues, but the fact of usage of the vulnerable oracle is the issue of contracts in scope.
Protocol and users can lose funds in different ways:
Mint of EURO with non-market prices.
Delayed liquidations of undercollateralised vaults.
Liquidations of overcollateralised vaults.
See detailed explanations below.
Manual review
Use an oracle without vulnerabilities (see how current could be improved below).
PriceCalculator contract which is formally out-of-scope and provided to explain that the issue above is valid.There is no check for stale prices in PriceCalculator (declared dependency).
Chainlink recommends checking that the latestTimestamp of an answer is recent enough:
Your application should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay.
Currently, it is not checked.
Possible scenario:
The last price for BTC in Chainlink is 30k.
Falling market, the price becomes 20k.
Users can mint EUROs for a higher than real price.
The protocol loses funds due to undercollateralised positions.
The protocol will lose funds due to undercollateralised positions.
Manual review
Check that the price is not older than:
Heartbeat for stable coin collaterals.
N (for example, 1 hour) for volatile collaterals (BTC, ETH, etc).
There are no restrictions on the while loop cycle count in PriceCalculator::avgPrice, which is called during the call of SmartVaultV3::euroCollateral. This can lead to spending all available gas and reverting liquidation transactions.
Several intro facts that can lead to the DDOS:
Compatibilities include Any EVM chains with live Chainlink data feeds and live Uniswap pools.
Some chains could have quite a small block gas limit or even decrease it.
From Known Issues: Length of accepted tokens array is unchecked, but this TokenManager contract is managed by us and there is unlikely to be more than 5-10 tokens in this array.
Chainlink facts:
Aggregators receive updates from the oracle network only when the Deviation Threshold or Heartbeat Threshold triggers an update during an aggregation round (see https://docs.chain.link/architecture-overview/architecture-decentralized-model?parent=dataFeeds#aggregator).
Could be checked by running this script on Arbitrum on this block npx hardhat node --fork https://arb1.arbitrum.io/rpc --fork-block-number 166096430
Could be checked in Arbitrum with WBTC aggregator (declared dependency)
SmartVaultV3::undercollateralised is called twice in SmartVaultManagerV5::liquidateVault:
The first time in the check try vault.undercollateralised() returns (bool _undercollateralised) {.
The second time inside of vault.liquidate().
Let's suppose the worst case when we have continuous price fluctuations leading to updating asset prices every Chainlink round. The current average period is 4 hours (see PriceCalculator::tokenToEurAvg).
For 1 asset only, for optimal reading prices for these 4 hours, 4 * 60 * 60 / 30 * 7415 = 3_607_200 of gas will be spent.
For 5 accepted tokens, it will cost 3607200 * 5 = 18_036_000, which is more than the proposed Gnosis block gas limit (Since there is a loop by the accepted tokens in SmartVaultV3::euroCollateral).
For 10 tokens: 3607200 * 10 = 36_072_000, which is more than the Ethereum block gas limit.
0x6FF84e5bF2Cff6cF1f23071A6f4e2e169D535e97 - is PriceCalculator from the declared dependencies list
On a falling market, delay of liquidations means that the protocol will not be able to sell collateral at a price enough to cover the debt.
Manual review
PriceCalculator should be improved.
Chainlink uses a VWAP algorithm (https://chain.link/education-hub/twap-vs-vwap) for price calculations and manual avg calculation could be skipped.
In the PriceCalculator::avgPrice in average price used price outside the 4h interval
There is no guarantee that there is an answer for round n-1 if there is an answer for n and n > 1.
In the PriceCalculator::avgPrice in average price counts price outside the 4h interval:
First checked TS of the current round.
Then retrieved previous round and added value of this round right away, without checking TS.
So average price is always incorrect (since price outside the interval ui used in calculation). It is minor issue, but with the Chainlink issue below it becomes dangerous.
Chainlink uses a proxy for seamless change of aggregator implementations.
Proxy roundId = uint80((uint256(phaseId) << 64) | aggregatorRoundId) where phaseId is the generation of the implementation and aggregatorRoundId is the real round id. So there could be a huge gap between round ids where there is no data. This is not handled in PriceCalculator::avgPrice which is a declared dependency:
So, a possible situation:
Chainlink deploys a new implementation (for example generation 6)
Chainlink changes the implementation less than 4h after deployment (for example, on round 3 of the new implementation)
Right after the change of implementation, avgPrice will try to jump to the past for more than 3 rounds and will retrieve round 6<<64 data with zero answer, which will spoil the average value
See this PoC, where we emulate a request made right after the change of implementation. It should be run on the Ethereum mainnet with hours=4 and pricefeed=0xf4030086522a5beea4988f8ca5b36dbc97bee88c (wbtc/usd)
The result is 1407855500000 (1 btc = 14078 usd) - an incorrect value.
Console output:
I didn't find examples when Chainlink really changed the implementation less than 4h after the deployment of a new aggregator. Because of this, I set the severity to MEDIUM and provided a synthetic example, but there is no guarantee that they will not do it in the future.
In the worst case, PriceCalculator will return price = realPrice/2 that will allow the liquidation of healthy positions.
Manual review
PriceCalculator should be improved.
Chainlink uses a VWAP algorithm (https://chain.link/education-hub/twap-vs-vwap) for price calculations and manual avg calculation could be skipped
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.