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: 42/185
Findings: 2
Award: $112.05
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: T1MOH
Also found by: 0x1337, 0xNaN, 0xepley, 0xluckhu, 0xmystery, 7siech, Aamir, AlexCzm, Aymen0909, DanielArmstrong, GREY-HAWK-REACH, HChang26, Jiamin, Juntao, QiuhaoLi, Ruhum, SBSecurity, Varun_05, Weed0607, adam-idarrha, adriro, ast3ros, ayden, circlelooper, crack-the-kelp, crunch, cryptothemex, deepplus, mahdirostami, max10afternoon, osmanozdemir1, rouhsamad, rvierdiiev, trachev, xAriextz, zhaojie
36.0335 USDC - $36.03
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L136
When a user deposits asset
with LRTDepositPool.sol#depositAsset
function, the minted amount of rsETHToken
to the user is miscalculated. So the user loses funds. And when a user deposits asset
, rsETHPrice
becomes bigger and bigger.
LRTDepositPool.sol#depositAsset
is as follows.
File: LRTDepositPool.sol 119: function depositAsset( 120: address asset, 121: uint256 depositAmount 122: ) 123: external 124: whenNotPaused 125: nonReentrant 126: onlySupportedAsset(asset) 127: { 128: // checks 129: if (depositAmount == 0) { 130: revert InvalidAmount(); 131: } 132: if (depositAmount > getAssetCurrentLimit(asset)) { 133: revert MaximumDepositLimitReached(); 134: } 135: 136: if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { 137: revert TokenTransferFailed(); 138: } 139: 140: // interactions 141: uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); 142: 143: emit AssetDeposit(asset, depositAmount, rsethAmountMinted); 144: }
As we can see above, L136
transfers asset token from msg.sender
first and then L141
mints rsETHToken
to the user.
_mintRsETH
function is as follows.
File: LRTDepositPool.sol 151: function _mintRsETH(address _asset, uint256 _amount) private returns (uint256 rsethAmountToMint) { 152: (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount); 153: 154: address rsethToken = lrtConfig.rsETH(); 155: // mint rseth for user 156: IRSETH(rsethToken).mint(msg.sender, rsethAmountToMint); 157: }
L152
calculates the amount of rsETHToken
to mint.
getRsETHAmountToMint
function is as follows.
File: LRTDepositPool.sol 095: function getRsETHAmountToMint( 096: address asset, 097: uint256 amount 098: ) 099: public 100: view 101: override 102: returns (uint256 rsethAmountToMint) 103: { 104: // setup oracle contract 105: address lrtOracleAddress = lrtConfig.getContract(LRTConstants.LRT_ORACLE); 106: ILRTOracle lrtOracle = ILRTOracle(lrtOracleAddress); 107: 108: // calculate rseth amount to mint based on asset amount and asset exchange rate 109: rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice(); 110: }
L109 calculates rsETHPrice
through lrtOracle.getRSETHPrice()
.
lrtOracle.getRSETHPrice()
is as follows.
File: LRTOracle.sol 52: function getRSETHPrice() external view returns (uint256 rsETHPrice) { 53: address rsETHTokenAddress = lrtConfig.rsETH(); 54: uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply(); 55: 56: if (rsEthSupply == 0) { 57: return 1 ether; 58: } 59: 60: uint256 totalETHInPool; 61: address lrtDepositPoolAddr = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL); 62: 63: address[] memory supportedAssets = lrtConfig.getSupportedAssetList(); 64: uint256 supportedAssetCount = supportedAssets.length; 65: 66: for (uint16 asset_idx; asset_idx < supportedAssetCount;) { 67: address asset = supportedAssets[asset_idx]; 68: uint256 assetER = getAssetPrice(asset); 69: 70: uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); 71: totalETHInPool += totalAssetAmt * assetER; 72: 73: unchecked { 74: ++asset_idx; 75: } 76: } 77: 78: return totalETHInPool / rsEthSupply; 79: }
As we can see above, rsETHPrice
equals to the eth
amount of total supported assets divided by total supply of rsETHToken. Therefore, this means eth
price about a unit amount of rsETHToken.
But in LRTDepositPool.sol#depositAsset
function, first transfered asset
token from msg.sender
and then calculated rsETHPrice
. So the rsETHPrice
at this time, is bigger than real.
Threfore, the amount of rsETHToken
to mint is smaller than real.
Manual Review
LRTDepositPool.sol#depositAsset
function has to be modified as follows.
function depositAsset( address asset, uint256 depositAmount ) external whenNotPaused nonReentrant onlySupportedAsset(asset) { // checks if (depositAmount == 0) { revert InvalidAmount(); } if (depositAmount > getAssetCurrentLimit(asset)) { revert MaximumDepositLimitReached(); } - if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { - revert TokenTransferFailed(); - } // interactions uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); + if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { + revert TokenTransferFailed(); + } emit AssetDeposit(asset, depositAmount, rsethAmountMinted); }
Token-Transfer
#0 - c4-pre-sort
2023-11-16T19:16:58Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T19:17:06Z
raymondfam marked the issue as duplicate of #62
#2 - c4-judge
2023-11-29T21:25:05Z
fatherGoose1 marked the issue as satisfactory
#3 - c4-judge
2023-12-01T19:00:05Z
fatherGoose1 changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-12-04T15:31:41Z
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/LRTConfig.sol#L121
When the manager update the strategy for an asset, the protocol may lose funds deposited already to the previous strategy.
At the same time, the price of rsETH
may fall down.
LRTConfig.sol#updateAssetStrategy
function is as follows.
File: LRTConfig.sol 109: function updateAssetStrategy( 110: address asset, 111: address strategy 112: ) 113: external 114: onlyRole(DEFAULT_ADMIN_ROLE) 115: onlySupportedAsset(asset) 116: { 117: UtilLib.checkNonZeroAddress(strategy); 118: if (assetStrategy[asset] == strategy) { 119: revert ValueAlreadyInUse(); 120: } 126: assetStrategy[asset] = strategy; 127: }
That is, the above function does not check the existence of funds already deposited to the old strategy in the case that the assetStrategy[asset] !== address(0)
.
Thus, the protocol may not refund assets from the old strategy.
The price of rsETH
is determined in the LRTOracle.sol#getRSETHPrice
function by the ratio of the total price of assets to the totalSupply()
of rsETH
.
Therefore, the value of rsETH
may fall, depending on the amount of funds deposited to the old strategy.
The example is as follows.
A
deposits 100eth of asset1
to LRTDepositPool
and receive 100eth of rsETH
, the user B
deposits 100eth of asset2
and receive 100eth of rsETH
.updateAssetStrategy
and set assetStrategy[asset1] = strategy1
.asset1
to strategy1
through NodeDelegator
. At this time, the price of rsETH
will be getRSETHPrice() == 1 ether
.updateAssetStrategy
and update to assetStrategy[asset1] = strategy2
and the protocol lose 100eth of asset1
.rsETH
will fall down to getRSETHPrice() == 0.5 ether
. Thus, at this time, if user C
deposits 100eth of asset1
to the deposit pool, he will receive 200eth of rsETH
.Manual Review
Modify LRTConfig.sol#updateAssetStrategy
function to check the existence of funds deposited.
At first, add the followin function to LRTDepositPool.sol
.
File: interfaces/ILRTDepositPool.sol 21: + function getTotalAssetStaked(address asset) external view returns (uint256);
File: LRTDepositPool.sol 91: + function getTotalAssetStaked(address asset) 92: + external 93: + view 94: + onlySupportedAsset(asset) 95: + returns (uint256 amount) 96: + { 97: + uint256 ndcsCount = nodeDelegatorQueue.length; 98: + for (uint256 i; i < ndcsCount;) { 99: + amount += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); 100: + unchecked { 101: + ++i; 102: + } 103: + } 104: + }
Next, add the following check to LRTConfig.sol#updateAssetStrategy
function.
File: LRTConfig.sol 109: function updateAssetStrategy( 110: address asset, 111: address strategy 112: ) 113: external 114: onlyRole(DEFAULT_ADMIN_ROLE) 115: onlySupportedAsset(asset) 116: { 117: UtilLib.checkNonZeroAddress(strategy); 118: if (assetStrategy[asset] == strategy) { 119: revert ValueAlreadyInUse(); 120: } 121: + if (assetStrategy[asset] !== address(0)) { 122: + address depositPoolAddress = getContract(LRTConstants.LRT_DEPOSIT_POOL); 123: + require(ILRTDepositPool(depositPoolAddress).getTotalAssetStaked() == 0, 'Please, refund staked assets first'); 124: + } 125: assetStrategy[asset] = strategy; 126: }
Error
#0 - c4-pre-sort
2023-11-16T04:24:39Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T04:24:59Z
raymondfam marked the issue as duplicate of #197
#2 - c4-judge
2023-12-01T17:24:57Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-04T16:41:52Z
fatherGoose1 changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-12-08T17:25:35Z
fatherGoose1 marked the issue as satisfactory