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: 80/185
Findings: 2
Award: $12.73
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: m_Rassska
Also found by: 0x1337, 0xAadi, 0xHelium, 0xLeveler, 0xblackskull, 0xbrett8571, 0xepley, 0xffchain, 0xluckhu, 0xmystery, 0xrugpull_detector, 0xvj, ABAIKUNANBAEV, Aamir, AerialRaider, Amithuddar, Bauchibred, Bauer, CatsSecurity, Cryptor, Daniel526, Draiakoo, Eigenvectors, ElCid, GREY-HAWK-REACH, Inspecktor, Juntao, King_, LinKenji, Madalad, MaslarovK, Matin, MatricksDeCoder, McToady, Noro, PENGUN, Pechenite, Phantasmagoria, RaoulSchaffranek, SBSecurity, SandNallani, Shaheen, Soul22, Stormreckson, T1MOH, Tadev, TeamSS, TheSchnilch, Topmark, Tumelo_Crypto, Udsen, Yanchuan, ZanyBonzy, _thanos1, adeolu, adriro, alexfilippov314, almurhasan, amaechieth, anarcheuz, ayden, baice, bareli, boredpukar, bronze_pickaxe, btk, cartlex_, catellatech, chaduke, cheatc0d3, circlelooper, codynhat, crack-the-kelp, critical-or-high, debo, deepkin, desaperh, dipp, eeshenggoh, evmboi32, ge6a, gesha17, glcanvas, gumgumzum, hals, hihen, hunter_w3b, jasonxiale, joaovwfreire, ke1caM, leegh, lsaudit, marchev, merlinboii, niser93, osmanozdemir1, paritomarrr, passion, pep7siup, phoenixV110, pipidu83, poneta, ro1sharkm, rouhsamad, rvierdiiev, sakshamguruji, seerether, shealtielanz, soliditytaker, spark, squeaky_cactus, stackachu, supersizer0x, tallo, taner2344, turvy_fuzz, twcctop, ubl4nk, wisdomn_, xAriextz, zach, zhaojie, zhaojohnson, ziyou-
2.7592 USDC - $2.76
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L57
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.
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.
Manual review.
Consider changing the way of how total deposits are calculated.
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 ?
🌟 Selected for report: 0xVolcano
Also found by: 0xAnah, 0xhex, 0xta, ABAIKUNANBAEV, JCK, Raihan, Rickard, Sathish9098, ZanyBonzy, hunter_w3b, lsaudit, rumen, shamsulhaq123, tala7985, unique
9.97 USDC - $9.97
Number | Optimization Details | Instances |
---|---|---|
[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.
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) }
In terms of gas, it's better to use pre-calculated contract address instead of address(this)
:
assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));
whenNotPaused
modifier is not used.LRTOracle.sol
pause() and
unpause()functions are initialized but
whenNotPaused` 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(); } }
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; }
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