Kelp DAO | rsETH - ABAIKUNANBAEV's results

A collective DAO designed to unlock liquidity, DeFi and higher rewards for restaked assets through liquid restaking.

General Information

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

Kelp DAO

Findings Distribution

Researcher Performance

Rank: 80/185

Findings: 2

Award: $12.73

QA:
grade-b
Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

2.7592 USDC - $2.76

Labels

bug
downgraded by judge
grade-b
insufficient quality report
QA (Quality Assurance)
duplicate-116
Q-115

External Links

Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L57

Vulnerability details

Impact

In LRTDepositPool.sol contract, there is a function called getAssetCurrentLimit() that calculates the difference between lrtConfig.depositLimitByAsset(asset) and getTotalAssetDeposits(asset). However, due to the possibility of increasing the total asset deposits above the deposit limit level, there is a chance that the function will underflow and, consequently, depositAsset() will revert as well.

Proof of Concept

getTotalAssetDeposits(asset) consists of 3 components: assetLyingInDepositPool, assetLyingInNDCs`:

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L47-51

function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) { (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) = getAssetDistributionData(asset); return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer); }

The problem is that only the assetLyingInDepositPool can be controlled in the system due to the depositAsset() check:

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L132-134

if (depositAmount > getAssetCurrentLimit(asset)) { revert MaximumDepositLimitReached(); }

But assetLyingInNDCs and assetLyingInDepositPool can be increased due to the fact that the tokens can be sent to the node delegator contracts directly and without using transferAssetToNodeDelegator(). This can effectively lead to a situation where the limit may reach the level (even if it's not intentionally by the node delegator) where it's more than the current deposit limit for a specific type of asset. depositAsset() where getAssetCurrentLimit() is used will also underflow. Node delegators can also send tokens directly and put them into the strategy without using deposit pool functionality at all.

Tools Used

Manual review.

Consider changing the way of how total deposits are calculated.

Assessed type

Other

#0 - c4-pre-sort

2023-11-15T23:42:19Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2023-11-15T23:42:44Z

raymondfam marked the issue as duplicate of #116

#2 - c4-judge

2023-11-29T19:14:31Z

fatherGoose1 changed the severity to QA (Quality Assurance)

#3 - c4-judge

2023-11-29T19:14:47Z

fatherGoose1 marked the issue as grade-b

#4 - rodiontr

2023-12-03T05:25:49Z

I think it can easily be medium as it effectively allows to front-run every time the depositLimit is reached.

There can be even a scenario where one user deposits and after his deposit depositLimit is reached. And another user just front-runs him by directly sending tokens to the NDC or deposits himself and make his tx revert. This would allow implement this attack anytime when the depositLimit is almost reached and somebody will always lose gas money due to the tx revert.

So it’s "leak value with a hypothetical attack path with stated assumptions, but external requirements".

And if the issue #479 is considered as medium and it only shows “something that can happen in the future but it’s not the fact that it would happen”, and moreover, it’d be governance issue if the admins add such asset with different decimals. In contrary, this problem is already present in this code, why this is not treated as a medium?

thanks for reviewing! Would like to hear your thoughts on this

#5 - fatherGoose1

2023-12-04T16:29:00Z

@rodiontr This does not warrant medium severity. It is not an "attack". A permissionless protocol does not care which user is depositing. It is not in their interest to protect the right for users' deposit transactions to successfully complete as opposed to a griefer.

On top of that, I struggle to see any incentive for a griefer to perform exercise this action. It's an expensive maneuver that is only present under certain conditions, with no benefit to the griefer.

#6 - rodiontr

2023-12-04T17:29:41Z

@rodiontr This does not warrant medium severity. It is not an "attack". A permissionless protocol does not care which user is depositing. It is not in their interest to protect the right for users' deposit transactions to successfully complete as opposed to a griefer.

On top of that, I struggle to see any incentive for a griefer to perform exercise this action. It's an expensive maneuver that is only present under certain conditions, with no benefit to the griefer.

Thank you for the answer. I think there are plenty of gas-griefing cases with no beneifit to a griefer. It (this issue) just points out the losses for the user as every time the deposit limit is close enough, somebody can always lose money as their deposit will be front-runned and they will not be able to not only deposit but also they will lose their funds. In addition to this, the probability of something happening as the criteria should be secondary factor. If there is a deposit limit, it’s supposed to be reached multiple times or so for different assets and every time somebody will be gas-griefed.

And also about permissionlessness: I agree that protocol shouldn’t always care about how transactions are placed but it creates functionality (deposit limits) and therefore creates new attack vectors (even if they are not intentional) and it should be created in a way when users don’t lose anything. So if this problem can be mitigated multiple ways, why it should stay present ?

Findings Information

Awards

9.97 USDC - $9.97

Labels

bug
G (Gas Optimization)
grade-b
sufficient quality report
G-14

External Links

Gas Optimizations List

NumberOptimization DetailsInstances
[G-01]Consider not using nonReentrant modifier where the possibility of reentrancy is 0.4
[G-02]Use hard-coded address instead of address(this) to save gas.6
[G-03]Consider not using pausability in the contract where whenNotPaused modifier is not used.1
[G-04]Use assembly to implement for-loops.6
[G-05]Use assembly to implement for address(0).3

Total 5 issues.

[G‑01] Consider not using nonReentrant modifier where the possibility of reentrancy is 0.

Although it's considered as the best practice, it's recommended to use it with the functions with callbacks are possible such as ERC1155 or ERC777 functionality, for example:

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L125

function depositAsset( address asset, uint256 depositAmount ) external whenNotPaused nonReentrant onlySupportedAsset(asset) }

[G‑02] Use hard-coded address instead of address(this) to save gas.

In terms of gas, it's better to use pre-calculated contract address instead of address(this):

assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));

[G‑03] Consider not using pausability in the contract where whenNotPaused modifier is not used.

LRTOracle.sol pause() and unpause()functions are initialized butwhenNotPaused` modifier is never used. Therefore, these 2 functions and pausability feature may not be implemented as it just consumes more gas and not actually represent any logic:

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L101-109

function pause() external onlyLRTManager { _pause(); } /// @dev Returns to normal state. Contract must be paused function unpause() external onlyLRTAdmin { _unpause(); } }

[G-04] Use assembly to implement for-loops.

That's much cheaper to implement for-loops using assembly even though the readability suffers a bit.

for (uint256 i = 0; i < strategiesLength;) { assets[i] = address(IStrategy(strategies[i]).underlyingToken()); assetBalances[i] = IStrategy(strategies[i]).userUnderlyingView(address(this)); unchecked { ++i; }

[G-05] Use assembly to check for address(0).

https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L27

UtilLib.checkNonZeroAddress(lrtConfigAddr);

#0 - c4-pre-sort

2023-11-17T03:49:44Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-12-01T16:18:06Z

fatherGoose1 marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter