Open code423n4 opened 1 year ago
0xSorryNotSorry marked the issue as high quality report
0xSorryNotSorry marked the issue as primary issue
toshiSat marked the issue as sponsor acknowledged
This is known and exepected behavior. We feel like having all-or-nothing failures like this simplify the logic overall and make things safer at the expense of some edge cases where deposit can fail. We can always upgrade the contract if it becomes a problem,.
Picodes marked the issue as selected for report
Lines of code
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L71-L91 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L113-L119 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L140-L153 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L66-L127 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/SfrxEth.sol#L60-L106 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/WstEth.sol#L61
Vulnerability details
Impact
When
stake()/unstake()/rebalanceToWeights()
, if any one of the derivatives fails todeposit()/withdraw()
, the whole function will revert, causing DoS. The impacts include:Proof of Concept
In
stake()
, each derivative iteration,ethPerDerivative()
anddeposit()
will be called:In
unstake()
, each derivative is iterated towithdraw()
:In
rebalanceToWeights()
, each derivative is iterated towithdraw()
and thendeposit()
:For each of the current derivatives, there are several different scenarios where the
ethPerDerivative()/deposit()/withdraw()
could fail.SfrxEth.sol
redeem()
could fail due to not enough allowance.61: IsFrxEth(SFRX_ETH_ADDRESS).redeem( 62: _amount, 63: address(this), 64: address(this) 65: );
exchange()
could fail due tominOut
requirement.77: IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange( 78: 1, 79: 0, 80: frxEthBalance, 81: minOut 82: );
Below is the frxETHMinter contract:
If
submitPaused
is turned on, this deposit function will revert.Reth.sol
rethAddress()
andgetAddress()
rethAddress()
is called in multiple places:But it could return wrong address or
addr(0)
, since the referredgetAddress()
could return unexpected result.addressStorage[_key]
can be reset or deleted. Then the whole function call will revert.Below is the RocketStorage.sol:
rethAddress()
is referred inwithdraw()/deposit()/ethPerDerivative()/balance()
:getAddress()
will also influencepoolCanDeposit()
, which could revertdeposit()
and the view functionethPerDerivative()
:burn()
There is no guarantee that the function
burn()
will succeed.Because in Reth contract code below, the execution may fail in several cases:
burn()
->getTotalCollateral()
->getContractAddress()
->getAddress()
might fail due to the same reason aboverequire(ethBalance >= ethAmount)
could fail due to low balanceburn()
->withdrawDepositCollateral()
->getContractAddress()
,withdrawExcessBalance()
both could fail for same reason as abovemsg.sender.transfer()
could fail due to not enough gas (2300 limit)// RocketTokenRETH.sol: 98-101 function getTotalCollateral() override public view returns (uint256) { RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); return rocketDepositPool.getExcessBalance().add(address(this).balance); }
// RocketBase.sol: 112-119 function getContractAddress(string memory _contractName) internal view returns (address) { // Get the current contract address address contractAddress = getAddress(keccak256(abi.encodePacked("contract.address", _contractName))); // Check it require(contractAddress != address(0x0), "Contract not found"); // Return return contractAddress; }
// RocketTokenRETH.sol: 152-159 function withdrawDepositCollateral(uint256 _ethRequired) private { // Check rETH contract balance uint256 ethBalance = address(this).balance; if (ethBalance >= _ethRequired) { return; } // Withdraw RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); rocketDepositPool.withdrawExcessBalance(_ethRequired.sub(ethBalance)); }
As long as any one of the above code failed, the whole
stake()/unstake()/rebalanceToWeights()
will revert, and users' fund would be locked until the external dependency is resolved, the contract will lose the core functionality.Tools Used
Manual analysis.
Recommended Mitigation Steps
Use
try/catch
to skip the failed function call, then the contract will be more robust to unexpected situations. In case ofdeposit()
, redistribute the fund into the other derivatives according to the weights might be an option, since re-balance will be done regularly. Forwithdraw()
, maybe temporarily record the missed amount, and give the user opportunity to retrieve later.