Platform: Code4rena
Start Date: 10/11/2023
Pot Size: $28,000 USDC
Total HM: 5
Participants: 185
Period: 5 days
Judge: 0xDjango
Id: 305
League: ETH
Rank: 9/185
Findings: 2
Award: $978.59
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: m_Rassska
Also found by: 0xDING99YA, Aamir, Bauchibred, CatsSecurity, HChang26, PENGUN, SBSecurity, adriro, almurhasan, anarcheuz, circlelooper, d3e4, deth, jayfromthe13th
902.5718 USDC - $902.57
https://github.com/code-423n4/2023-11-kelp/blob/main/src/oracles/ChainlinkPriceOracle.sol#L37-L38
In the getAssetPrice function we need to use a heartbeat check with the lastRoundData function. The absence of a Chainlink heartbeat check could potentially lead to several issues to the overall protocol.
The use of the lastRoundData function in this context is essential to ensure that the data being used is current and accurate. Without this Chainlink heartbeat check, the system is vulnerable to several risks. Firstly, there is the danger of relying on stale or outdated price data, which could lead to incorrect operational decisions. This is particularly critical in dynamic market environments where price accuracy is paramount.
Additionally, the absence of a heartbeat check opens up the possibility for malicious manipulation. An attacker could potentially freeze the price feed, providing data that benefits their operations while disadvantaging others. Furthermore, the system's heavy reliance on external data sources becomes a liability if these sources go offline or stop providing updates. In such scenarios, the contract might continue to operate on the last available price, potentially leading to significant financial losses. Therefore, implementing a heartbeat check is not just a technical enhancement but a fundamental requirement to safeguard the protocol against these issues.
-Setup Scenario: In this hypothetical scenario, a user engages with the Kelp product by depositing Ethereum (ETH) and receiving RSETH tokens. The pricing mechanism for this transaction is managed by the LRTOracle contract, which relies on the ChainlinkPriceOracle contract's getAssetPrice function to determine the market price of Liquid Staking Tokens (LST).
-Issue with Chainlink Oracle: A critical situation arises when the Chainlink oracle, responsible for providing LST/ETH price updates, experiences a technical failure and goes offline. Consequently, the price feed halts, freezing the last recorded price at 1 LST = 0.01 ETH.
-Market Price Variation: In the market, the value of LST experiences a significant increase, reaching 1 LST = 0.02 ETH. However, due to the offline status of the Chainlink oracle, this price change is not reflected in the Kelp product's system.
-Redemption Process: A user, unaware of the oracle's failure, initiates a redemption of their RSETH tokens for LST. The LRTOracle contract, in response, calls upon the getAssetPrice function to ascertain the current LST price.
-Impact of Stale Data: The getAssetPrice function, lacking a mechanism to verify the freshness of data (a heartbeat check), erroneously returns the outdated price of 1 LST = 0.01 ETH.
-Resulting Under-Redemption: This incorrect price leads to the user receiving a lower quantity of LST tokens than the market value dictates, potentially causing a financial loss.
-Trust Implications: Should the user realize the discrepancy in token quantity after the Chainlink oracle resumes operation and updates the price, it could severely undermine their trust in the Kelp product and the broader protocol.
Foundry
Modify the getAssetPrice function to include a heartbeat check. This can be done by calling the latestRoundData function from the Chainlink oracle and checking the timestamp of the latest price data.
function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) { (,int price,,uint timeStamp,) = AggregatorInterface(assetPriceFeed[asset]).latestRoundData(); require(block.timestamp - timeStamp < acceptableDelay, "Price data is stale"); return uint(price); }
Oracle
#0 - c4-pre-sort
2023-11-16T21:29:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T21:30:03Z
raymondfam marked the issue as duplicate of #609
#2 - c4-judge
2023-12-01T17:43:14Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-08T17:32:44Z
fatherGoose1 marked the issue as duplicate of #584
#4 - c4-judge
2023-12-08T17:48:56Z
fatherGoose1 marked the issue as satisfactory
#5 - c4-judge
2023-12-08T18:55:04Z
fatherGoose1 changed the severity to 3 (High Risk)
🌟 Selected for report: crack-the-kelp
Also found by: 0xDING99YA, 0xffchain, Aymen0909, Bauchibred, DanielArmstrong, Pechenite, Stormy, T1MOH, ZanyBonzy, ast3ros, bLnk, chaduke, crack-the-kelp, deepkin, deth, jasonxiale, jayfromthe13th, lsaudit, nmirchev8, osmanozdemir1, roleengineer, tallo, zhaojie
76.0163 USDC - $76.02
https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L59 https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L122
The NodeDelegator contract tracks the balance of assets deposited into a strategy, which is configured in the LRTConfig contract. However, the strategy associated with a particular asset can be changed using the updateAssetStrategy function in the LRTConfig contract. This could potentially lead to loss of funds if the strategy is changed while there are still assets in the old strategy.
If the strategy for an asset is changed while there are still funds in the old strategy, those funds could be lost or stuck. This is because the NodeDelegator contract might not have a reference to the old strategy anymore, and therefore cannot interact with it.
-Initial Setup:
The LRT platform uses the NodeDelegator contract to manage assets and strategies. The platform supports a variety of assets, including rsETH, and uses different strategies to optimize the yield for each asset. The strategies are implemented in separate contracts and can be changed by the LRT manager using the LRTConfig contract.
-Depositing Assets:
The LRT manager decides to deposit a certain amount of rsETH into the platform. They call the depositAssetIntoStrategy function in the NodeDelegator contract, which transfers the rsETH from the NodeDelegator contract to the current strategy for rsETH, let's call it StrategyA.
-Changing the Strategy:
Later, the LRT manager decides to change the strategy for rsETH to a new strategy, StrategyB, because it offers a higher yield. They call the updateAssetStrategy function in the LRTConfig contract, which updates the strategy for rsETH in the NodeDelegator contract.
-The Issue:
However, they forget to withdraw the rsETH from StrategyA before changing the strategy. Now, the NodeDelegator contract thinks that the strategy for rsETH is StrategyB and has no reference to StrategyA anymore. This means it cannot interact with StrategyA to withdraw the rsETH.
The rsETH that was deposited into StrategyA is now stuck. Even though the rsETH is technically still in the platform, it cannot be accessed or managed by the NodeDelegator contract. If the LRT manager tries to withdraw the rsETH, the NodeDelegator contract will try to withdraw it from StrategyB, not from StrategyA, and the withdrawal will fail because there is no rsETH in StrategyB.
This scenario illustrates how changing the strategy for an asset while there are still funds in the old strategy can lead to loss of funds. The NodeDelegator contract loses the reference to the old strategy and cannot interact with it anymore, which means it cannot withdraw the funds from the old strategy.
Foundry
Withdraw funds before updating strategy: Before updating the strategy for an asset, ensure that all funds are withdrawn from the current strategy. This could be enforced in the updateAssetStrategy function in the LRTConfig contract.
function updateAssetStrategy(address asset, address newStrategy) external onlyLRTManager { // Ensure all funds are withdrawn from the current strategy address currentStrategy = assetStrategy(asset); require(IStrategy(currentStrategy).balanceOf(address(this)) == 0, "Funds still in current strategy"); // Update the strategy _assetStrategies[asset] = newStrategy; }
Keep track of all used strategies: Modify the NodeDelegator contract to keep track of all strategies that have ever been used for each asset. This would allow it to interact with old strategies even after they have been replaced.
mapping(address => address[]) private _assetStrategies; function getAssetStrategies(address asset) external view returns (address[] memory) { return _assetStrategies[asset]; } function depositAssetIntoStrategy(address asset) external override { // Deposit into the current strategy address currentStrategy = lrtConfig.assetStrategy(asset); _assetStrategies[asset].push(currentStrategy); // ... }
Restrict who can call updateAssetStrategy: Ensure that only trusted accounts can call the updateAssetStrategy function to prevent unauthorized changes to the strategies.
function updateAssetStrategy(address asset, address newStrategy) external onlyLRTManager { // Update the strategy _assetStrategies[asset] = newStrategy; }
Error
#0 - c4-pre-sort
2023-11-16T23:00:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T23:00:48Z
raymondfam marked the issue as duplicate of #197
#2 - c4-judge
2023-12-01T17:24:51Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-08T17:26:36Z
fatherGoose1 marked the issue as satisfactory