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: 138/185
Findings: 1
Award: $2.76
🌟 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
There is a deposit limit for every kind of asset, and the amount of the deposited asset cannot exceed this limit. Before a user deposits an asset, they need to call the getTotalAssetDeposits
function to determine the current limit of asset deposit. This value is equal to the capacity limit minus the amount of the asset already deposited. Assets are stored in three locations: depositPool, NodeDelegators, and EigenLayer. The amount of the deposited asset is the sum of the assets in these three locations.
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L47-L51
function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) { (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) = getAssetDistributionData(asset); return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer); } function getAssetCurrentLimit(address asset) public view override returns (uint256) { return lrtConfig.depositLimitByAsset(asset) - getTotalAssetDeposits(asset); }
There are multiple NodeDelegators in the system, and each NodeDelegator can stake assets in EigenLayer. When calculating the amount of assets, it involves iterating through all NodeDelegators and summing up the assets. https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L71-L89
function getAssetDistributionData(address asset) public view override onlySupportedAsset(asset) returns (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) { // Question: is here the right place to have this? Could it be in LRTConfig? assetLyingInDepositPool = IERC20(asset).balanceOf(address(this)); uint256 ndcsCount = nodeDelegatorQueue.length; >> for (uint256 i; i < ndcsCount;) { >> assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]); >> assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); >> unchecked { >> ++i; >> } >> } }
Only the admin can add NodeDelegators to the system, and this is accomplished through the addNodeDelegatorContractToQueue function. However, when the admin adds a NodeDelegator to the system, there is no check for whether the NodeDelegator is a duplicate. https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L162-L176
function addNodeDelegatorContractToQueue(address[] calldata nodeDelegatorContracts) external onlyLRTAdmin { uint256 length = nodeDelegatorContracts.length; if (nodeDelegatorQueue.length + length > maxNodeDelegatorCount) { revert MaximumNodeDelegatorCountReached(); } for (uint256 i; i < length;) { UtilLib.checkNonZeroAddress(nodeDelegatorContracts[i]); nodeDelegatorQueue.push(nodeDelegatorContracts[i]); emit NodeDelegatorAddedinQueue(nodeDelegatorContracts[i]); unchecked { ++i; } } }
This introduces a vulnerability. If a NodeDelegator is added redundantly, the getAssetDistributionData function will redundantly calculate the amount of assets in both the NodeDelegator and EigenLayer, resulting in an overestimation of the total asset amount. As a consequence, the amount of asset that users can deposit cannot reach the deposit limit..
Here is a test case: (1) The admin invokes the addNodeDelegatorContractToQueue function to add three NodeDelegators, but all three NodeDelegators are nodeDelegatorContractOne. (2) Manager sets the deposit limit to 10e18 (3) Alice deposits 2e18 rETH into the depositPool. (4) The Manager transfers all rETH from the depositPool to the NodeDelegator. (5) Alice deposits an additional 5e18 rETH into the depositPool.
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.21; import { BaseTest } from "./BaseTest.t.sol"; import { LRTDepositPool } from "src/LRTDepositPool.sol"; import { RSETHTest, ILRTConfig, UtilLib, LRTConstants } from "./RSETHTest.t.sol"; import { ILRTDepositPool } from "src/interfaces/ILRTDepositPool.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import { console } from "forge-std/Test.sol"; contract LRTOracleMock { function getAssetPrice(address) external pure returns (uint256) { return 1e18; } function getRSETHPrice() external pure returns (uint256) { return 1e18; } } contract MockNodeDelegator { function getAssetBalance(address) external view returns (uint256) { return 0; } } contract LRTDepositPoolTest is BaseTest, RSETHTest { LRTDepositPool public lrtDepositPool; function setUp() public virtual override(RSETHTest, BaseTest) { super.setUp(); // deploy LRTDepositPool ProxyAdmin proxyAdmin = new ProxyAdmin(); LRTDepositPool contractImpl = new LRTDepositPool(); TransparentUpgradeableProxy contractProxy = new TransparentUpgradeableProxy( address(contractImpl), address(proxyAdmin), "" ); lrtDepositPool = LRTDepositPool(address(contractProxy)); // initialize RSETH. LRTCOnfig is already initialized in RSETHTest rseth.initialize(address(admin), address(lrtConfig)); vm.startPrank(admin); // add rsETH to LRT config lrtConfig.setRSETH(address(rseth)); // add oracle to LRT config lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(new LRTOracleMock())); // add minter role for rseth to lrtDepositPool rseth.grantRole(rseth.MINTER_ROLE(), address(lrtDepositPool)); vm.stopPrank(); } } contract LRTDepositPoolAddDupDelegators is LRTDepositPoolTest { address public nodeDelegatorContractOne; address[] public nodeDelegatorQueueProspectives; function setUp() public override { super.setUp(); // initialize LRTDepositPool lrtDepositPool.initialize(address(lrtConfig)); nodeDelegatorContractOne = address(new MockNodeDelegator()); nodeDelegatorQueueProspectives.push(nodeDelegatorContractOne); nodeDelegatorQueueProspectives.push(nodeDelegatorContractOne); nodeDelegatorQueueProspectives.push(nodeDelegatorContractOne); // add manager role within LRTConfig vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); vm.stopPrank(); // add node delegator contracts to queue vm.startPrank(admin); lrtDepositPool.addNodeDelegatorContractToQueue(nodeDelegatorQueueProspectives); vm.stopPrank(); } function test_AddDupDelegators() external { vm.prank(manager); lrtConfig.updateAssetDepositLimit(address(rETH), 10 ether); console.log("Asset current limit: %s", lrtDepositPool.getAssetCurrentLimit(address(rETH))); // deposit 2 ether rETH vm.startPrank(alice); rETH.approve(address(lrtDepositPool), type(uint256).max); lrtDepositPool.depositAsset(address(rETH), 2 ether); vm.stopPrank(); console.log("Asset current limit before delegator: %s", lrtDepositPool.getAssetCurrentLimit(address(rETH))); // transfer 2 ether rETH to node delegator contract one vm.startPrank(manager); lrtDepositPool.transferAssetToNodeDelegator(0, address(rETH), 2 ether); vm.stopPrank(); console.log("Asset current limit after delegator %s", lrtDepositPool.getAssetCurrentLimit(address(rETH))); vm.prank(alice); lrtDepositPool.depositAsset(address(rETH), 5 ether); } }
From the test results, it can be observed that after the Manager deposits rETH into the NodeDelegator, the value of the asset current limit becomes 4e18. When Alice attempts to deposit an additional 5e18 rETH into the depositPool, the transaction reverts.
Running 1 test for test/LRTDepositPoolTest.t.sol:LRTDepositPoolAddDupDelegators [FAIL. Reason: MaximumDepositLimitReached()] test_AddDupDelegators() (gas: 309055) Logs: Asset current limit: 10000000000000000000 Asset current limit before delegator: 8000000000000000000 Asset current limit after delegator 4000000000000000000 Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 7.96ms Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in test/LRTDepositPoolTest.t.sol:LRTDepositPoolAddDupDelegators [FAIL. Reason: MaximumDepositLimitReached()] test_AddDupDelegators() (gas: 309055) Encountered a total of 1 failing tests, 0 tests succeeded
Foundry
In the addNodeDelegatorContractToQueue function, check the newly added NodeDelegator for (1) duplicates within the nodeDelegatorContracts itself and (2) presence in the nodeDelegatorQueue. If there are duplicate NodeDelegators, revert the transaction.
Context
#0 - c4-pre-sort
2023-11-16T19:44:15Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-16T19:44:23Z
raymondfam marked the issue as duplicate of #36
#2 - c4-judge
2023-11-29T21:35:52Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-29T21:43:53Z
fatherGoose1 marked the issue as grade-b