Ceres Farm Contracts
Cantina Security Report
Organization
- @ceresfarm
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Low Risk
3 findings
3 fixed
0 acknowledged
Informational
9 findings
7 fixed
2 acknowledged
Gas Optimizations
1 findings
1 fixed
0 acknowledged
Low Risk3 findings
_tendTrigger return True when there are idle asset but does not deposit the idle assset.
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
ladboy233
Description
According to the vault strategy contract comment guide
/** * @dev Optional function for strategist to override that can * be called in between reports. * * If '_tend' is used tendTrigger() will also need to be overridden. * * This call can only be called by a permissioned role so may be * through protected relays. * * This can be used to harvest and compound rewards, deposit idle funds, * perform needed position maintenance or anything else that doesn't need * a full report for. * * EX: A strategy that can not deposit funds without getting * sandwiched can use the tend when a certain threshold * of idle to totalAssets has been reached. * * This will have no effect on PPS of the strategy till report() is called. * * @param _totalIdle The current amount of idle funds that are available to deploy. */ function _tend(uint256 _totalIdle) internal virtual {} /** * @dev Optional trigger to override if tend() will be used by the strategy. * This must be implemented if the strategy hopes to invoke _tend(). * * @return . Should return true if tend() should be called by keeper or false if not. */ function _tendTrigger() internal view virtual returns (bool) { return false; }The
_tendTriggerreturnTrueif there are idle asset but does not handle the idle asset,so if a off-chain keeper that relies on the
_tendTrigger()to give right signal can keep trying to triggertendrebalance even the tvl is not out of range.Recommendation
function _tendTrigger() internal view override returns (bool) { // _tend needs to be triggered if the strategy LTV is outside configured range return _isLtvOutOfRange(); }If the intention is only to trigger rebalance when ltv is out of range, then only return
Trueif ltv is out of range.The keepers can always call
rebalanceStrategyto trigger idle asset deposit.Inconsistent reentrancy protection may allow reentrancies
Severity
- Severity: Low
Submitted by
Optimum
Description
CeresLeveragedStrategyinheritsBaseStrategywhich delegates calls toTokenizedStrategy. The latter uses storage based reentrancy protection mechanism based on the storage variable ofS.enteredwhileCeresLeveragedStrategyrelies onReentrancyGuardTransient.We want to block any potential reentrancy between any given pair of functions of the deployed contract and currently it is theoretically possible for cross function reentrancies to occur between state changing functions from
CeresLeveragedStrategyandTokenizedStrategy.As a concrete example, let's take the scenario of
rebalanceStrategy()being called, this function is calling many external contracts, in case one of these calls can call back, it might call a function ofTokenizedStrategy(likewithdraw()for instance) via theBaseStrategyfallback function and might manipulate theCeresLeveragedStrategycontract by doing so.Recommendation
Consider changing
CeresLeveragedStrategyto use the storage based reentrancy protection mechanism that's already used byTokenizedStrategyinstead of the current one which is based on transient storage.Cantina
Fixed by implementing the reviewer's recommendation.
StrategyFactory.newStrategy(): deployments values can be over-written
Severity
- Severity: Low
Submitted by
Optimum
Description
newStrategy()allows anyone to initialize a new Strategy contract, then the newly deployed address is stored indeployments:deployments[_asset] = address(_newStrategy);The issue here is that anyone can call this function for a given asset with a different strategy and it will over-write the value stored for this asset inside
deployments.Recommendation
Consider restricting access to this function to privileged users only.
Cantina
Fixed by implementing the reviewer's recommendation.
Informational9 findings
Fund cannot be withdraw on time if the utilization rate on silo v2 engine is high
State
- Acknowledged
Severity
- Severity: Informational
≈
Likelihood: Low×
Impact: Low Submitted by
ladboy233
Description
https://silodocs2.netlify.app/docs/users/using-silo/supply#withdrawing
the code may withdraw collateral from
siloduring rebalance or emergency withdraw.However, when the collateral are borrowed out in silo, the withdraw fails when utilization rate is high
Suppliers may withdraw their deposit, including accrued interest, at any time. The exception is when liquidity is fully utilized where interest rates are expected to be high to encourage repayments and attract new deposits.
Recommendation
Add code to monitor utilization rate and if utilization rate is high, skip withdraw collateral from Silo to avoid transaction revert.
Explicit upper limit check is missing for configTargetLtv
Severity
- Severity: Informational
Submitted by
Optimum
Description
function setTargetLtv(uint256 _targetLtv) external onlyManagement { // Target LTV should be less than maxLtv allowed uint256 maxLtv = getMaxLtvSilo(); if (_targetLtv + LTV_BUFFER > maxLtv) revert LibError.AboveMaxValue(maxLtv); configTargetLtv = _targetLtv; emit TargetLtvSet(_targetLtv);}As we can see,
configTargetLtvis not explicitly checked to be less thanWAD, instead the check relies ongetMaxLtvSilo()max value to be WAD (10^18).Later in the code, we have two instances of subtraction that will cause a revert in case
configTargetLtv >= WAD:uint256 maxLoan = (totalAssetsInBorrowToken * configTargetLtv) / (WAD - configTargetLtv);uint256 targetDebt = (_convertAssetToBorrowToken(totalAssets - _amount) * configTargetLtv) / (WAD - configTargetLtv);Recommendation
For better code readability and no external reliance in case Silo updates their code, consider adding an explicit check in
setTargetLtv()to make sureconfigTargetLtvis less thanWAD.Cantina
Fixed by implementing the reviewer's recommendation.
CeresSwapper: Access to main functions can be limited to the CeresLeveragedStrategy only
Severity
- Severity: Informational
Submitted by
Optimum
Description
CeresSwapperis a non-upgradeable contract used byCeresLeveragedStrategyas a wrapper to support token swapping functionality.CeresSwapperis not meant to serve other accounts/contracts besidesCeresLeveragedStrategyand the main reason behind deploying it as a separate contract (instead of a library) is to maintain future flexibility to replace it in the future with a new version. Having theCeresSwappermain functionality callable by any address increases attack surface without any significant upside in contrast to the well known principal of least privileges better described in least-privilege.md.Recommendation
Consider restricting access to the main state changing functions of
CeresSwapper:swapFrom,swapUsingAggregator,swapTotoCeresLeveragedStrategyonly. Noting that it will make a circular dependency issue around initialization, it will require having an initialize function in theCeresSwapperthat will store the address of theCeresLeveragedStrategycontract.Cantina
Fixed by implementing the reviewer's recommendation.
CeresSwapper: internal functions used for swaps return values are not used
Severity
- Severity: Informational
Submitted by
Optimum
Description
The following functions return values that are not currently used:
_pendleSwapTokenForPt()_pendleSwapPtForToken()_algebraV4SwapExactIn()
Recommendation
Consider removing these return values from these functions.
Cantina
Fixed by implementing the reviewer's recommendation.
CeresSwapper: Missing events for state changing functions
Severity
- Severity: Informational
Submitted by
Optimum
Description
The following functions are missing event emissions:
swapFrom()swapUsingAggregator()swapTo()setSwapPath()setSwapProvider()setSwapAggregator()
Recommendation
Consider adding events to reflect state changes.
Cantina
Fixed by implementing the reviewer's recommendation.
CeresSwapper: The usage of low-level arbitrary external calls is discouraged
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Optimum
Description
Both
_kyberCachedSwap()and_kyberAggregatorSwap()use a storage address (router) for low level external calls. The caller of the two functions can specify any function in theroutercontract. Although it is not a fully arbitrary call, we still think this ability should be limited to reduce surface area.if (canBeScaled) { (bool isSuccess, ) = router.call(updatedSwapData); require(isSuccess, LibError.SwapFailed());} else { revert LibError.ScaledInputFailed();}Recommendation
Consider replacing the arbitrary call with the actual interface function(s) call of the Kyber contract, this restriction will restrict the flexibility of the
CeresSwappercontract which is not upgradeable, but since this contract should not hold any tokens post transaction, and can be set in the strategy contract, you can easily replace it with a new one in case changes are needed.CeresSwapper: Missing reentrancy guards
Severity
- Severity: Informational
Submitted by
Optimum
Description
swapFrom(),swapUsingAggregator(),swapTo()are missing reentrancy protection, although calls to external contracts are being made inside each of these functions.Recommendation
To reduce surface area, consider implementing reentrancy guards in the functions mentioned above.
Cantina
Fixed by implementing the reviewer's recommendation.
Minor code improvements
Severity
- Severity: Informational
Submitted by
Optimum
Description
CeresLeveragedStrategy:PENDLE_ROUTERis an immutable variable that is initialized but not used in the contract.CeresSwapper:swapTo()is missing Natspec documentation.
Recommendation
- Consider removing
PENDLE_ROUTERfrom declaration and assignment in the constructor ofCeresLeveragedStrategy. - Add Natspec documentation to
swapTo().
Cantina
Fixed by implementing the reviewer's recommendation.
CeresLeveragedStrategy is not initializing ceresSwapper during construction
Severity
- Severity: Informational
Submitted by
Optimum
Description
The constructor of
CeresLeveragedStrategyis missing the initialization ofceresSwapperwhich is supposed to happen only later insidesetSwapper(). In case the deployer forgets to callsetSwapper()right after deployment, many code paths in the contract will revert, mainlyrebalanceStrategy()which will delay potential earning of yield for depositors.Recommendation
Consider setting the value of
ceresSwapperin the constructor.Cantina
Fixed by implementing the reviewer's recommendation.
Gas Optimizations1 finding
CeresSwapper: Missing storage variables caching
Severity
- Severity: Gas optimization
Submitted by
Optimum
Description
Both
_algebraV4SwapExactIn()and_algebraV4SwapExactOut()read twice from storage although the value is already stored inswapPathas we can see:bytes memory swapPath = tokenSwapPath[tokenPairHash][SwapType.ALGEBRA_V4_SWAPX];if (swapPath.length > 0) { // Use the stored swap path for the token pair IV3SwapRouter.ExactOutputParams memory exactOutputParams = IV3SwapRouter.ExactOutputParams({ path: tokenSwapPath[tokenPairHash][SwapType.ALGEBRA_V4_SWAPX], recipient: address(this), amountOut: amountOut, amountInMaximum: maxAmountIn }); actualAmountIn = swapRouter.exactOutput(exactOutputParams);} else {Recommendation
Consider using the value of the cached
swapPathinstead.Cantina
Fixed by implementing the reviewer's recommendation.