Coinshift: Chainlink Integration
Cantina Security Report
Organization
- @coinshift
Engagement Type
Cantina Reviews
Period
-
Repositories
Researchers
Findings
Low Risk
3 findings
2 fixed
1 acknowledged
Informational
11 findings
4 fixed
7 acknowledged
Gas Optimizations
5 findings
2 fixed
3 acknowledged
Low Risk3 findings
Prices can only be used from max 1 day old
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
Prices can only be used from max 1 day old, due to several checks:
The functions
getPrice(),getPriceInfo(),getPriceInfoscalls()all call the following functions:_readRound()_validateAndNormalize()_chainlinkIsFrozen()
The function
_chainlinkIsFrozen()checks if the price has been updated < MAX_PRICE_AGE (e.g. 1 day). This means only prices upto 1 day ago can be retrieved.Additionally there are checks in
IUSPCHub::processSubscriptions()andIUSPCHub::_processSingleRedemption()which also only allow timestamp uptoMAX_PRICE_AGEago.Recommendation
If older prices are ever relevant (possibly in non standard situations where the contracts have been paused for a day), then consider updating the checks or have a workaround.
Coinshift
Acknowledged. This is intentional behavior, staleness policy is by design. The Hub also independently enforces
MAX_PRICE_AGEat processing time. Both contracts have the 24 hour window.Edge case for roundId - 1
State
- Fixed
PR #14
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
In function
_checkDeviation()the calculation ofcurrentResponse.roundId - 1isn't correct if the underlying aggregater has changed. In that situation high bits ofroundIdare updated and the lower bits part ofroundIdwill be set to 1.Recommendation
This situation can be detected by the check
uint64(previousRoundId) == 0.The most straightforward solution is to revert, because finding the highest
roundIdof the previous aggregater is difficult.Consider changing the code to:
-if (currentResponse.roundId <= 1 || ...) return;+if ( uint64(currentResponse.roundId) <= 1 || ... ) return; // strip the high bitsansweredInRound is deprecated
State
- Fixed
PR #14
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The value of
answeredInRoundis deprecated, see the chainlink documentation. Some oracles might return a value of 0, which will result in a revert during the check.Recommendation
Consider ignoring
answeredInRound.
Informational11 findings
l2-sequencer-feeds might be useful
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The uptime of the L2 isn't taken into account in the
Pricercontract. The project indicated that the current plans are to deploy on ethereum mainnet.Recommendation
If the protocol will be deployed on a L2, doublecheck the usefulness of the
l2-sequencer-feeds.Coinshift
Acknowledged. We will be deploying on eth mainnet. L2 sequencer uptime checks are not applicable.
Several functions, events and errors are not exposed in IPricer.sol
State
- Fixed
PR #14
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Several functions, events and errors are not exposed in
IPricer.sol. This makes interfacing with them more difficult.Recommendation
Consider add the function definitions to
IPricer.soland moving the errors toIPricer.sol.Several underlying init functions are not called from initialize()
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Several underlying
initfunctions are not called frominitialize(). The functions are currenlty empty, but that might be changed in the future.Recommendation
Consider also calling:
__ERC165_init()__Context_init()
Coinshift
Achknowledged. These are currently empty no ops. We'll review init chains when upgrading oz dependencies.
Unusable deposit-time round forces pricing at a different time
State
- Acknowledged
Severity
- Severity: Informational
≈
Likelihood: Low×
Impact: Low Submitted by
rvierdiiev
Description
Deposits and redemptions are processed with a Chainlink round id(
priceId) chosen to match the time of the operation.If that round fails validation (e.g.
DeviationTooHigh,StalePriceData,PriceNotFound), it is not possible to use it for that operation:- The operation can not be processed with the deposit-time priceId.
- The only way to process it is for the admin to call
processSubscriptions/processRedemptionswith a different priceId — i.e. a different Chainlink round, which corresponds to a different time than when the deposit (or redemption) actually occurred.
So when the correct round for the deposit/redemption time is unusable, the admin is forced to price the user at another moment in time, which can be seen as a temporal mismatch or fairness concern (user is minted/redeemed at a price from a time other than their operation time).
Recommendation
- Document that if the round at deposit/redemption time cannot be used (e.g. due to deviation or staleness), the operation can only be processed by using another round.
Coinshift
Acknowledged. This is Intentional behavior. The system should halt rather than process at stale prices. Adding an admin override increases attack surface for an extreme scenario.
The admin gathers
roundIdsoff-chain, if anyroundIdfails validation the batch reverts and admin resubmits with validroundIds.Function setChainlinkFeed() doesn't emit all changes
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The function
setChainlinkFeed()doesn't emit all changes.Recommendation
Consider also emitting the old and new
decimals.Coinshift
Acknowledged. Feed decimals are queryable on chain from the feed contract directly. Not worth changing the event signature for data that is already accessible.
Impractical values can be set
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Impractical values can be set, which will limit any changes to the prices.
maxPriceDeviationBpscan be set to 0minAllowedPricecan be set to the same values asmaxAllowedPrice
Recommendation
Consider disallowing to set impractical values.
Conshift
Acknowledged. It is on a trust based system, admin is trusted with role gated access.
Rapid price changes in combination with frequent oracle updates won't trigger deviations
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
When rapid price changes occur and the oracle is frequently updated the price difference between two updates can be small enough to not trigger deviations. Also when the price flattens out the prices difference will certainly be low between oracle updates.
Recommendation
Decide what actions to take during and direct after rapid price changes. Consider taking the time between oracle updates into account.
Coinshift
Acknowledged. This behaviour is expected, time weighting will add significant complexity.
fallbackDecimals is never used in _getCurrentChainlinkResponse()
State
- Fixed
PR #14
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The
try/catchin function_getCurrentChainlinkResponse()directly returns. This means the transaction will directly revert andfallbackDecimalswill never be used.Recommendation
Doublecheck the intended action. If this is intentional then consider removing
fallbackDecimals.Function _getRoundChainlinkResponse() doesn't check decimals()
State
- Fixed
PR #14
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Function
_getRoundChainlinkResponse()doesn't checkdecimals(), while function_getCurrentChainlinkResponse()does perform this check. This is inconsistent.Recommendation
If you want anticipate a change in
decimals()then it should be implemented consistently.And to be fully consistent the following check, which is also done in
setChainlinkFeed(), could also be added:+ if (decimals > TARGET_DECIMALS) revert UnsupportedFeedDecimals();Implicit value success = false
State
- Fixed
PR #14
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The functions
_getCurrentChainlinkResponse()and_getRoundChainlinkResponse()leave the value ofresponse.successin their default value, which isfalse. This values is interpreted by the caller of the function to know the function has failed.However this behaviour might not be obvious for readers of the code.
Recommendation
Consider adding a comment:
-catch { ... }+catch { ... } // leave response.success == falseSimilar code _checkDeviation() and _isValidDeviationReference()
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The checks in
_checkDeviation()and_isValidDeviationReference()are very similar. Because the checks are not trivial, having it in two places makes maintenance of the code more difficult.Recommendation
Consider combining the checks. The check for
_chainlinkIsFrozen()could also be added.Coinshift
Acknowledged. It is doing different error handling:
_validateAndNormalize()reverts on bad data_isValidDeviationReference()returns false (skip)
Combining them into one method with a revert-or-skip flag reduces clarity without improving safety.
Gas Optimizations5 findings
_disableInitializers() call in the constructor is redundant
State
- Fixed
PR #14
Severity
- Severity: Gas optimization
≈
Likelihood: Low×
Impact: Low Submitted by
rvierdiiev
Description
_disableInitializers()call in the constructor can be removed as the same code exists inside the constructor ofAccessControlledUUPSUpgradeableparent contract.Recommendation
Remove redundant functionality.
Redundant check in _normalizePrice()
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
The check for
decimals == TARGET_DECIMALSis very unlikely to trigger in practice becausedecimalsusually are 8 or 18, which is smaller thanTARGET_DECIMALS.Without this statement, the code will still function correctly and some gas will be saved.
Recommendation
Consider removing the statement:
- if (decimals == TARGET_DECIMALS) return rawPrice;Coinshift
Acknowledged. We will keep the check, as removing the defensive guard doesn't save significant gas.
Array length can be cached in getPriceInfos()
State
- Fixed
PR #14
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
In the
forloop, the value of_priceIds.lengthis calculated for every cycle. This can be optimized.Recommendation
Consider caching the value of
_priceIds.length.Redundant overflow checks
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
There are a few checks that prevent overflows during multiplications. Solidity will also catch these types of errors so the checks aren't really necessary. Removing them will save some gas.
Recommendation
Consider removing the checks.
Coinshift
Acknowledged. Custom errors are intentionally used because they are clearer than a raw panic for debugging and monitoring.
Check in function _checkPriceBounds() can be optimized
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function
_checkPriceBounds()every time checksminPrice == 0 || maxPrice < minPrice. This checks can be done one time during configuration which will save same gas.Recommendation
Consider moving this check to
setBoundsCheckEnabled().Note: the check is already present in
setMaxPriceDeviationBps()function _checkPriceBounds(...) ... { ...- if (minPrice == 0 || maxPrice < minPrice) revert InvalidPriceBounds(); ...}function setBoundsCheckEnabled(...) ... {+ if (minPrice == 0 || maxPrice < minPrice) revert InvalidPriceBounds(); // have to be set correctly first ...}Coinshift
Acknowledged. It protects against the case where bounds are set and then are somehow corrupted then enabled. Gas costs are negligible in this case.