Platform: Code4rena
Start Date: 31/03/2022
Pot Size: $75,000 USDC
Total HM: 7
Participants: 42
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 5
Id: 102
League: ETH
Rank: 1/42
Findings: 6
Award: $42,069.40
🌟 Selected for report: 4
🚀 Solo Findings: 3
🌟 Selected for report: cmichel
23950.8585 USDC - $23,950.86
https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/oracle/ScalingPriceOracle.sol#L136 https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/oracle/ScalingPriceOracle.sol#L113
The oracle does not correctly compound the monthly APRs - it resets on fulfill
.
Note that the oraclePrice
storage variable is only set in _updateCPIData
as part of the oracle fulfill
callback.
It's set to the old price (price from 1 month ago) plus the interpolation from startTime
to now.
However, startTime
is reset in requestCPIData
due to the afterTimeInit
modifier, and therefore when Chainlink calls fulfill
in response to the CPI request, the timeDelta = block.timestamp - startTime
is close to zero again and oraclePrice
is updated to itself again.
This breaks the core functionality of the protocol as the oracle does not track the CPI, it always resets to 1.0
after every fulfill
instead of compounding it.
In addition, there should also be a way for an attacker to profit from the sudden drop of the oracle price to 1.0
again.
As an example, assume oraclePrice = 1.0 (1e18)
, monthlyAPR = 10%
. The time elapsed is 14 days. Calling getCurrentOraclePrice()
now would return 1.0 + 14/28 * 10% = 1.05
.
requestCPIData
. This resets startTime = now
.getCurrentOraclePrice()
now would return 1.0
again as timeDelta
(and priceDelta
) is zero: oraclePriceInt + priceDelta = oraclePriceInt = 1.0
.fulfill
is called it sets oraclePrice = getCurrentOraclePrice()
which will be close to 1.0
as the timeDelta
is tiny.The oraclePrice
should be updated in requestCPIData()
not in fulfill
.
Cover this scenario of multi-month accumulation in tests.
#0 - ElliotFriedman
2022-04-07T18:25:45Z
Oracle price does compound per this line of code: https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/oracle/ScalingPriceOracle.sol#L197-L198
#1 - ElliotFriedman
2022-04-13T19:18:40Z
This is the only valid critical finding we have seen so far! Great work cmichel!
1940.0195 USDC - $1,940.02
The RateLimited.setBufferCap
function first updates the buffer and then sets the new cap, but does not apply the new cap to the updated buffer.
Meaning, the updated buffer value can be larger than the new buffer cap which should never be the case.
Actions consuming more than the new buffer cap can be performed.
function _setBufferCap(uint256 newBufferCap) internal { // @audit still uses old buffer cap, should set buffer first _updateBufferStored(); uint256 oldBufferCap = bufferCap; bufferCap = newBufferCap; emit BufferCapUpdate(oldBufferCap, newBufferCap); }
Update the buffer after setting the new cap:
function _setBufferCap(uint256 newBufferCap) internal { - _updateBufferStored(); uint256 oldBufferCap = bufferCap; bufferCap = newBufferCap; + _updateBufferStored(); emit BufferCapUpdate(oldBufferCap, newBufferCap); }
🌟 Selected for report: robee
Also found by: IllIllI, cmichel, georgypetrov
Judge @jack-the-pug is upgrading the following issue from a QA report (issue #30 ) to Medium risk:
Division by zero in isWithinDeviationThreshold if a is zero. This only seems to be the case if the oracle would return 0 for CPI and in this case, something is wrong anyway. Should still handle this error more gracefully.
#0 - CloudEllie
2022-04-27T21:36:14Z
Duplicate of #58
#1 - jack-the-pug
2022-05-03T03:01:55Z
Confirmed! Thanks @CloudEllie
🌟 Selected for report: cmichel
7185.2575 USDC - $7,185.26
The OracleRef
assumes that the backup oracle uses the same normalizer as the main oracle.
This generally isn't the case as it could be a completely different oracle, not even operated by Chainlink.
If the main oracle fails, the backup oracle could be scaled by a wrong amount and return a wrong price which could lead to users being able to mint volt cheap or redeem volt for inflated underlying amounts.
Should there be two scaling factors, one for each oracle?
#0 - ElliotFriedman
2022-04-07T21:08:26Z
This is a good catch as it exposes some underlying assumptions made about backup oracles, however we can assume that both oracles will use the same scaling factor and thus we will not need a second value for the backup oracle.
🌟 Selected for report: cmichel
7185.2575 USDC - $7,185.26
When the bufferCap
is updated for an address in _updateAddress
, the address's allowed buffer (bufferStored
) is replenished to the entire bufferCap
.
The address could frontrun the updateAddress
call and spend their entire buffer, then the buffer is replenished and they can spend their entire buffer a second time.
Keep the old buffer value, capped by the new bufferCap
:
+ uint256 newBuffer = individualBuffer(rateLimitedAddress); rateLimitData.lastBufferUsedTime = block.timestamp.toUint32(); rateLimitData.bufferCap = _bufferCap; rateLimitData.rateLimitPerSecond = _rateLimitPerSecond; - rateLimitData.bufferStored = _bufferCap; + rateLimitData.bufferStored = min(_bufferCap, newBuffer);
#0 - ElliotFriedman
2022-04-07T21:09:40Z
Good catch!
🌟 Selected for report: rayn
Also found by: 0xDjango, 0xkatana, 0xkowloon, BouSalman, CertoraInc, Dravee, Funen, Hawkeye, IllIllI, Jujic, Kenshin, Kthere, Meta0xNull, Sleepy, TerrierLover, async, aysha, berndartmueller, catchup, cccz, cmichel, csanuragjain, danb, defsec, georgypetrov, hake, hubble, kenta, kyliek, pauliax, rfa, robee, sahar, shenwilly, teryanarmen
498.4945 USDC - $498.49
isWithinDeviationThreshold
if a
is zero. This only seems to be the case if the oracle would return 0
for CPI and in this case, something is wrong anyway. Should still handle this error more gracefully.#0 - CloudEllie
2022-04-27T21:36:41Z
Per the judge @jack-the-pug, the second issue in this report is being upgraded to Medium, and set as a duplicate of issue #58 .
Please see issue #127 for details.