Platform: Code4rena
Start Date: 15/03/2024
Pot Size: $60,500 USDC
Total HM: 16
Participants: 43
Period: 21 days
Judge: hansfriese
Total Solo HM: 5
Id: 348
League: ETH
Rank: 24/43
Findings: 2
Award: $109.28
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: klau5
Also found by: 0xSecuri, Bauchibred, Bigsam, Infect3d, falconhoof, nonseodion, serial-coder
42.0292 USDC - $42.03
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L117-L129 https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibOracle.sol#L19-L67
Inconsistent price checks in getOraclePrice()
will lead to wrong prices being used with detrimental effects to stability of protocol .
In getOraclePrice()
, the returned price value for "other" oracles (e.g. JPY, XAU) is checked inconsistently.
Firstly, it is mentioned in the Known Issues
regarding heartbeats:
" 2 hours staleness means it can be somewhat out of date "
However, both the try
block and the catch
block are missing even any heartbeat check as it is stated in the docs that there should be here.
The try
block does price checks in OracleCircuitBreaker()
as below:
function oracleCircuitBreaker( uint80 roundId, uint80 baseRoundId, int256 chainlinkPrice, int256 baseChainlinkPrice, uint256 timeStamp, uint256 baseTimeStamp ) private view { >>> bool invalidFetchData = roundId == 0 || timeStamp == 0 || timeStamp > block.timestamp || chainlinkPrice <= 0 >>> || baseRoundId == 0 || baseTimeStamp == 0 || baseTimeStamp > block.timestamp || baseChainlinkPrice <= 0; if (invalidFetchData) revert Errors.InvalidPrice(); }
Secondly, there is a missing check in the catch
block that the timestamp is not equal to 0
, which is present in oracleCircuitBreaker()
i.e. timeStamp == 0
.
Thirdly, there is an inconsistent use of relational operators, where in the try
block there is a check for negative prices but in the catch
block the is just a check that the value is not equal to 0
.
In oracleCircuitBreaker()
we see the check chainlinkPrice <= 0
reverts if there is a negative price returned which, given that Chainlink's prices are int
values, is relevant.
However in the catch
block, as below, we see the check price == 0
.
if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice();
Regarding the heartbeat and timestamp issues, these are esstential missing checks which protect the protocol from using stale prices.
Regarding the inconsistent application of relational operators, this means that if there is a failure in the try
block because a negative price reverted there, that same incorrect price will be accepted and used in the catch
block.
Both of these issues will cause huge issues to the correct functioning of the protocol via incorrect prices being fetched.
Manual Review Foundry Testing
Given that different feeds have different heartbeats, create an Oracle data struct whereby their specific infomation can be stored and fetched to be used in the oracleCircuitBreaker()
and the catch
block.
Update the price check and add the timestamp check to the catch
block in getOraclePrice()
as:
} catch { // SOME CODE - if (roundID == 0 || price == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice(); + if (roundID == 0 || price <= 0 || timeStamp == 0 || timeStamp > block.timestamp) revert Errors.InvalidPrice(); // SOME CODE }
Oracle
#0 - c4-pre-sort
2024-04-05T21:17:35Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-04-05T21:17:40Z
raymondfam marked the issue as duplicate of #4
#2 - raymondfam
2024-04-05T21:18:14Z
See #8.
#3 - c4-judge
2024-04-12T16:57:54Z
hansfriese marked the issue as unsatisfactory: Out of scope
#4 - Qormatic
2024-04-17T11:21:56Z
Hi @hansfriese ,
Appeal
I believe that a mistake has been made in ruling this issue Out of Scope. May you please review below and advise if I am missing something?
Per lookout's comment in 8 which references the Readme :
Common known issue per readme: Oracle is very dependent on Chainlink, stale/invalid prices fallback to a Uniswap TWAP. 2 hours staleness means it can be somewhat out of date.
However none of the three separate vulnerabilities mentioned in this report could be considered to be covered by that statement and hence Out of Scope. Note: There is also a single Oracle issue in the 4naly3er report which also doesn't relate to the three issues in the report
ETH / USD
pricefeed because the ETH / USD
pricefeed does in fact utilise a 2 hour staleness check.
However the 'other' feeds have neither a 2 hour staleness check nor any other staleness check whatsoever.If that part of the statement is the basis for it's exclusion I argue that it is an incorrect exclusion.
timestamp
values in the try
and catch
blocks for 'other' pricefeeds. In the try
block there is a check timestamp == 0
, see here, to ensure tmestamp is not 0
.
In the catch
block this check is missing, see here, raising the possibility that a 0
timestamp value rejected in the try
block would be accepted in the catch
block.Known Issues does not appear to make this finding Out of Scope.
try
and catch
blocks for 'other' pricefeeds. In the try
block there is a check chainlinkPrice <= 0
to ensure returned price is not negative or 0
, see here.
In the catch
block this check is different price == 0
, see here, meaning that a negative value rejected in the try
block could be accepted in the catch
block.Known Issues does not appear to make this finding Out of Scope.
#5 - c4-judge
2024-04-19T14:10:28Z
hansfriese marked the issue as not a duplicate
#6 - c4-judge
2024-04-19T14:10:36Z
hansfriese changed the severity to QA (Quality Assurance)
#7 - c4-judge
2024-04-19T14:10:46Z
hansfriese marked the issue as grade-c
#8 - Qormatic
2024-04-19T15:48:29Z
@hansfriese what about the lack of a staleness check for other pricefeeds? 252 which only mentions staleness has just been Sponsor Confirmed?
The First vulnerability describes the total lack of a heartbeat / staleness check for 'other' pricefeeds. Where the Readme states "2 hours staleness means it can be somewhat out of date" it is referring to the ETH / USD pricefeed because the ETH / USD pricefeed does in fact utilise a 2 hour staleness check. However the 'other' feeds have neither a 2 hour staleness check nor any other staleness check whatsoever.
#9 - hansfriese
2024-04-21T08:48:06Z
Check #164 for details.
#10 - c4-judge
2024-04-21T08:48:20Z
hansfriese removed the grade
#11 - c4-judge
2024-04-21T08:48:30Z
This previously downgraded issue has been upgraded by hansfriese
#12 - c4-judge
2024-04-21T08:48:36Z
hansfriese marked the issue as satisfactory
#13 - c4-judge
2024-04-21T08:48:49Z
hansfriese marked the issue as duplicate of #164
#14 - c4-judge
2024-04-21T08:48:55Z
hansfriese marked the issue as partial-50
🌟 Selected for report: Infect3d
Also found by: Evo, LinKenji, XDZIBECX, falconhoof, foxb868, ilchovski, klau5, nonseodion
67.2468 USDC - $67.25
https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/libraries/LibShortRecord.sol#L27-L28 https://github.com/code-423n4/2024-03-dittoeth/blob/91faf46078bb6fe8ce9f55bcb717e5d2d302d22e/contracts/facets/RedemptionFacet.sol#L56-L211
Redemption proposals should be processed using an up to date price rather than a cached price which may be stale
In the docs it is stated here:
shortRecords
that are invalid to be proposed: if it's greater than the max allowable CR
proposeRedemption()
uses a cached price p.oraclePrice to calculate the collateral Ratio of Short Records being proposed.
The cached price may be using stale data so the CRs in the list of proposals may now have different CRs which violate the C.MAX_REDEMPTION_CR
check and therefore should never have been included in the slate.
function proposeRedemption( address asset, MTypes.ProposalInput[] calldata proposalInput, uint88 redemptionAmount, uint88 maxRedemptionFee ) external isNotFrozen(asset) nonReentrant { // SOME CODE p.oraclePrice = LibOracle.getPrice(p.asset); // SOME CODE p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; // SOME CODE }
Invalid Short Records are added to the slate. Paying off their debt increases their CR beyond the max allowed level which can affect stability of the protocol.
Manual Review Foundry Testing
Use getSavedOrSpotOraclePrice()
which will use getPrice()
if it's not stale to save gas or else return the spot price.
function proposeRedemption( address asset, MTypes.ProposalInput[] calldata proposalInput, uint88 redemptionAmount, uint88 maxRedemptionFee ) external isNotFrozen(asset) nonReentrant { // SOME CODE - p.oraclePrice = LibOracle.getPrice(p.asset); + p.oraclePrice = LibOracle.getSavedOrSpotOraclePrice(p.asset); // SOME CODE p.currentCR = currentSR.getCollateralRatio(p.oraclePrice); if (p.previousCR > p.currentCR || p.currentCR >= C.MAX_REDEMPTION_CR) continue; // SOME CODE }
Oracle
#0 - c4-pre-sort
2024-04-07T01:20:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-04-07T01:20:30Z
raymondfam marked the issue as duplicate of #114
#2 - raymondfam
2024-04-07T01:20:59Z
See #128.
#3 - c4-judge
2024-04-11T16:12:34Z
hansfriese marked the issue as satisfactory