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: 160/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
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L162-L176 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L144
Medium impact
It does not have big chances to happen and if it happens it can be solved by upgrading the LRTDepositPool
contract. However, if it happens, nobody would be able to deposit assets into the protocol until the deposit pool is upgraded.
If a contract that does not implement the function getAssetBalance(address)
is registered as a node delegator in LRTDepositPool
, nobody will be able to deposit assets until the contract gets upgraded, because there is no function to modify or remove an address inside the nodeDelegatorQueue
array.
Foundry POC:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.21; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import { LRTConfig, ILRTConfig } from "src/LRTConfig.sol"; import { RSETH } from "src/RSETH.sol"; import { LRTDepositPool } from "src/LRTDepositPool.sol"; import { LRTConstants } from "src/utils/LRTConstants.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; contract MockToken is ERC20 { constructor(string memory name, string memory symbol) ERC20(name, symbol) { } function mint(address to, uint256 amount) external { _mint(to, amount); } } contract LRTOracleMock { LRTConfig public lrtConfig; mapping(address asset => uint256 price) public prices; constructor(address lrtConfigAddr) { lrtConfig = LRTConfig(lrtConfigAddr); } function setAssetPrice(address asset, uint256 amount) external { prices[asset] = amount; } function getAssetPrice(address asset) public view returns (uint256) { return prices[asset]; } function getRSETHPrice() external view returns (uint256 rsETHPrice) { address rsETHTokenAddress = lrtConfig.rsETH(); uint256 rsEthSupply = RSETH(rsETHTokenAddress).totalSupply(); if (rsEthSupply == 0) { return 1 ether; } uint256 totalETHInPool; address lrtDepositPoolAddr = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL); address[] memory supportedAssets = lrtConfig.getSupportedAssetList(); uint256 supportedAssetCount = supportedAssets.length; for (uint16 asset_idx; asset_idx < supportedAssetCount;) { address asset = supportedAssets[asset_idx]; uint256 assetER = getAssetPrice(asset); uint256 totalAssetAmt = LRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); totalETHInPool += totalAssetAmt * assetER; unchecked { ++asset_idx; } } return totalETHInPool / rsEthSupply; } } contract AuditTests is Test { MockToken public stETH; MockToken public rETH; MockToken public cbETH; LRTConfig public lrtConfig; RSETH public rseth; LRTDepositPool public lrtDepositPool; LRTOracleMock public lrtOracle; address public manager = makeAddr("manager"); address public admin = makeAddr("admin"); address public alice = makeAddr("alice"); address public bob = makeAddr("bob"); address public carol = makeAddr("carol"); function setUp() public virtual { stETH = new MockToken("staked ETH", "stETH"); rETH = new MockToken("rETH", "rETH"); cbETH = new MockToken("cbETH", "cbETH"); ProxyAdmin proxyAdmin = new ProxyAdmin(); LRTConfig lrtConfigImpl = new LRTConfig(); TransparentUpgradeableProxy lrtConfigProxy = new TransparentUpgradeableProxy( address(lrtConfigImpl), address(proxyAdmin), "" ); RSETH tokenImpl = new RSETH(); TransparentUpgradeableProxy tokenProxy = new TransparentUpgradeableProxy( address(tokenImpl), address(proxyAdmin), "" ); LRTDepositPool contractImpl = new LRTDepositPool(); TransparentUpgradeableProxy contractProxy = new TransparentUpgradeableProxy( address(contractImpl), address(proxyAdmin), "" ); lrtConfig = LRTConfig(address(lrtConfigProxy)); lrtConfig.initialize(admin, address(stETH), address(rETH), address(cbETH), address(1)); lrtOracle = new LRTOracleMock(address(lrtConfig)); lrtDepositPool = LRTDepositPool(address(contractProxy)); lrtDepositPool.initialize(address(lrtConfig)); rseth = RSETH(address(tokenProxy)); rseth.initialize(address(admin), address(lrtConfig)); vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(lrtOracle)); lrtConfig.setContract(LRTConstants.LRT_DEPOSIT_POOL, address(lrtDepositPool)); lrtConfig.setRSETH(address(rseth)); rseth.grantRole(rseth.MINTER_ROLE(), address(lrtDepositPool)); vm.stopPrank(); // set prices for tokens lrtOracle.setAssetPrice(address(stETH), 1 ether); // 1 ETH = 1 stETH lrtOracle.setAssetPrice(address(rETH), 2 ether); // 2 ETH = 1 rETH lrtOracle.setAssetPrice(address(cbETH), 3 ether); // 3 ETH = 1 cbETH } function test_WrongNodeDelegatorRegistered() public { vm.startPrank(admin); WrongNodeDelegatorImplementation wrongNodeDelegator = new WrongNodeDelegatorImplementation(); address[] memory newNodes = new address[](1); newNodes[0] = address(wrongNodeDelegator); lrtDepositPool.addNodeDelegatorContractToQueue(newNodes); stETH.mint(alice, 1 ether); // Now nobody can depositAssets into the protocol vm.startPrank(alice); stETH.approve(address(lrtDepositPool), 1 ether); vm.expectRevert(); lrtDepositPool.depositAsset(address(stETH), 1 ether); vm.stopPrank(); } } contract WrongNodeDelegatorImplementation { // It does not contain getAssetBalance(address) }
Traces
Running 1 test for test/BaseTest.t.sol:AuditTests [PASS] test_WrongNodeDelegatorRegistered() (gas: 227024) Traces: [227024] AuditTests::test_WrongNodeDelegatorRegistered() ├─ [0] VM::startPrank(admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF]) │ └─ ← () ├─ [12464] → new WrongNodeDelegatorImplementation@0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395 │ └─ ← 62 bytes of code ├─ [70787] TransparentUpgradeableProxy::addNodeDelegatorContractToQueue([0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395]) │ ├─ [63686] LRTDepositPool::addNodeDelegatorContractToQueue([0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395]) [delegatecall] │ │ ├─ [9792] TransparentUpgradeableProxy::hasRole(0x0000000000000000000000000000000000000000000000000000000000000000, admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF]) [staticcall] │ │ │ ├─ [2694] LRTConfig::hasRole(0x0000000000000000000000000000000000000000000000000000000000000000, admin: [0xaA10a84CE7d9AE517a52c6d5cA153b369Af99ecF]) [delegatecall] │ │ │ │ └─ ← true │ │ │ └─ ← true │ │ ├─ emit NodeDelegatorAddedinQueue(prospectiveNodeDelegatorContract: WrongNodeDelegatorImplementation: [0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395]) │ │ └─ ← () │ └─ ← () ├─ [46588] MockToken::mint(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1000000000000000000 [1e18]) │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], value: 1000000000000000000 [1e18]) │ └─ ← () ├─ [0] VM::startPrank(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) │ └─ ← () ├─ [24599] MockToken::approve(TransparentUpgradeableProxy: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], 1000000000000000000 [1e18]) │ ├─ emit Approval(owner: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], spender: TransparentUpgradeableProxy: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF], value: 1000000000000000000 [1e18]) │ └─ ← true ├─ [0] VM::expectRevert() │ └─ ← () ├─ [20479] TransparentUpgradeableProxy::depositAsset(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 1000000000000000000 [1e18]) │ ├─ [19883] LRTDepositPool::depositAsset(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 1000000000000000000 [1e18]) [delegatecall] │ │ ├─ [3156] TransparentUpgradeableProxy::isSupportedAsset(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall] │ │ │ ├─ [2561] LRTConfig::isSupportedAsset(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [delegatecall] │ │ │ │ └─ ← true │ │ │ └─ ← true │ │ ├─ [1156] TransparentUpgradeableProxy::isSupportedAsset(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall] │ │ │ ├─ [561] LRTConfig::isSupportedAsset(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [delegatecall] │ │ │ │ └─ ← true │ │ │ └─ ← true │ │ ├─ [2558] MockToken::balanceOf(TransparentUpgradeableProxy: [0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF]) [staticcall] │ │ │ └─ ← 0 │ │ ├─ [2558] MockToken::balanceOf(WrongNodeDelegatorImplementation: [0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395]) [staticcall] │ │ │ └─ ← 0 │ │ ├─ [23] WrongNodeDelegatorImplementation::getAssetBalance(MockToken: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall] │ │ │ └─ ← "EvmError: Revert" │ │ └─ ← "EvmError: Revert" │ └─ ← "EvmError: Revert" ├─ [0] VM::stopPrank() │ └─ ← () └─ ← () Test result: ok. 1 passed; 0 failed; finished in 12.60ms
Manual review
Implement a node delegator validation when registering new nodes. Node delegator contracts would need to have this function:
function isNodeDelegator() external pure returns(bool){ return true; }
And the function to register new node delegators would be as follows:
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]); require(INodeDelegator(nodeDelegatorContracts[i]).isNodeDelegator(), "wrong node"); nodeDelegatorQueue.push(nodeDelegatorContracts[i]); emit NodeDelegatorAddedinQueue(nodeDelegatorContracts[i]); unchecked { ++i; } } }
Invalid Validation
#0 - c4-pre-sort
2023-11-16T04:53:36Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-16T04:53:50Z
raymondfam marked the issue as duplicate of #36
#2 - c4-judge
2023-11-29T21:35:51Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-29T21:42:53Z
fatherGoose1 marked the issue as grade-b