Platform: Code4rena
Start Date: 24/03/2023
Pot Size: $49,200 USDC
Total HM: 20
Participants: 246
Period: 6 days
Judge: Picodes
Total Solo HM: 1
Id: 226
League: ETH
Rank: 189/246
Findings: 1
Award: $13.13
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: brgltd
Also found by: 0x3b, 0xAgro, 0xGusMcCrae, 0xNorman, 0xRajkumar, 0xSmartContract, 0xTraub, 0xWagmi, 0xWaitress, 0xffchain, 0xhacksmithh, 0xkazim, 0xnev, 3dgeville, ArbitraryExecution, Aymen0909, BRONZEDISC, Bason, Bloqarl, BlueAlder, Brenzee, CodeFoxInc, CodingNameKiki, Cryptor, DadeKuma, DevABDee, Diana, Dug, Englave, Gde, Haipls, HollaDieWaldfee, Ignite, Infect3d, Jerry0x, Josiah, Kaysoft, Koko1912, KrisApostolov, Lavishq, LeoGold, Madalad, PNS, Rappie, RaymondFam, RedTiger, Rickard, Rolezn, Sathish9098, SunSec, T1MOH, UdarTeam, Udsen, Viktor_Cortess, Wander, adriro, ak1, alejandrocovrr, alexzoid, arialblack14, ayden, bin2chen, brevis, btk, c3phas, carlitox477, catellatech, ch0bu, chaduke, ck, climber2002, codeslide, descharre, dingo2077, ernestognw, fatherOfBlocks, favelanky, georgits, helios, hl_, inmarelibero, juancito, ks__xxxxx, lopotras, lukris02, m_Rassska, mahdirostami, maxper, nadin, navinavu, nemveer, p_crypt0, peanuts, pipoca, pixpi, qpzm, rbserver, reassor, roelio, rotcivegaf, scokaf, siddhpurakaran, slvDev, smaul, tnevler, tsvetanovv, turvy_fuzz, vagrant, wen, yac, zzzitron
13.1298 USDC - $13.13
The unstake()
function in SafEth.sol
does not check that the user balance is sufficient to deduct amount _safEthAmount
supplied as function input,
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L108
function unstake(uint256 _safEthAmount) external { require(pauseUnstaking == false, "unstaking is paused"); uint256 safEthTotalSupply = totalSupply(); uint256 ethAmountBefore = address(this).balance; for (uint256 i = 0; i < derivativeCount; i++) { // withdraw a percentage of each asset based on the amount of safETH uint256 derivativeAmount = (derivatives[i].balance() * _safEthAmount) / safEthTotalSupply; if (derivativeAmount == 0) continue; // if derivative empty ignore derivatives[i].withdraw(derivativeAmount); } _burn(msg.sender, _safEthAmount); uint256 ethAmountAfter = address(this).balance; uint256 ethAmountToWithdraw = ethAmountAfter - ethAmountBefore; // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: ethAmountToWithdraw}( "" ); require(sent, "Failed to send Ether"); emit Unstaked(msg.sender, ethAmountToWithdraw, _safEthAmount); }
until the _burn(address, uint)
function, which by then sufficient gas been wasted. there should be a require function at the start of the function which checks like so:
function unstake(uint256 _safEthAmount) external { require(pauseUnstaking == false, "unstaking is paused"); require(balanceOf(msg.sender)>= _safEthAmount, 'message here') .............. }
In the rebalance function in SafEth.sol,
function rebalanceToWeights() external onlyOwner { uint256 ethAmountBefore = address(this).balance; for (uint i = 0; i < derivativeCount; i++) { if (derivatives[i].balance() > 0) derivatives[i].withdraw(derivatives[i].balance()); } uint256 ethAmountAfter = address(this).balance; uint256 ethAmountToRebalance = ethAmountAfter - ethAmountBefore; for (uint i = 0; i < derivativeCount; i++) { if (weights[i] == 0 || ethAmountToRebalance == 0) continue; uint256 ethAmount = (ethAmountToRebalance * weights[i]) / totalWeight; // Price will change due to slippage derivatives[i].deposit{value: ethAmount}(); } emit Rebalanced(); }
there should an assert that checks if there is value staked in the system before continuing the process, because the rebalancing mechanism only comes into play when there is asset staked in the protocol. If there is no asset then this should fail as there is nothing to rebalance.
function rebalanceToWeights() external onlyOwner { require(totalSupply() > 0, 'message'); uint256 ethAmountBefore = address(this).balance; ............. }
An attempt is made on this at L#148 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L148
(weights[i] == 0 || ethAmountToRebalance == 0) continue
But this can be done earlier
In the addDerivative()
function , the first parameter of _contractAddress
is not validated when passed in, for example the allowed user might pass a zero address successfully and they would be no way to delete this, except to disable the weight on finding out.
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L182
a validation like so should be included:
function addDerivative( address _contractAddress, uint256 _weight ) external onlyOwner { require(_contractAddress != address(0), 'message'); ......................... }
The withdraw function on Reth.sol allows anyone the ability to remove eth stock in the contract, https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L107
function withdraw(uint256 amount) external onlyOwner { RocketTokenRETHInterface(rethAddress()).burn(amount); // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: address(this).balance}( "" ); require(sent, "Failed to send Ether"); }
Steps to replicate:
Mitigation:
function withdraw(uint256 amount) external onlyOwner { uint EthAmountBefore = address(this).balance; RocketTokenRETHInterface(rethAddress()).burn(amount); uint EthAmountAfter = address(this).balance; uintEthAmountToSend = EthAmountAfter - EthAmountBefore; // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: EthAmountToSend }( "" ); require(sent, "Failed to send Ether"); }
In temporary storing balances before and after making external calls, there is an inconsistent use of nomenclature in performing the same operation on multiple contracts.
uint256 sfrxBalancePre = IERC20(SFRX_ETH_ADDRESS).balanceOf(address(this)); frxETHMinterContract.submitAndDeposit{value: msg.value}(address(this)); uint256 sfrxBalancePost = IERC20(SFRX_ETH_ADDRESS).balanceOf(address(this));
uint256 rethBalance1 = rocketTokenRETH.balanceOf(address(this)); rocketDepositPool.deposit{value: msg.value}(); uint256 rethBalance2 = rocketTokenRETH.balanceOf(address(this));
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L121
uint256 ethAmountBefore = address(this).balance; .......................... uint256 ethAmountAfter = address(this).balance; uint256 ethAmountToWithdraw = ethAmountAfter - ethAmountBefore;
A more consistent naming nomenclature should be used for these operations. For example either going for the before and after suffixes or the pre and post suffixes and then using these all around the codebase as may warrant.
The return value of function deposit
on sfrxEth.sol returns the direct aritmetic balance of the post and pre balance like so:
function deposit() external payable onlyOwner returns (uint256) { .................... return sfrxBalancePost - sfrxBalancePre; }
but in similar operations on both Reth.sol and WstEth.sol it returns a variable which stores the result of the aritmetic operation.
function deposit() external payable onlyOwner returns (uint256) { .................... uint256 rethMinted = rethBalance2 - rethBalance1; return (rethMinted); }
function deposit() external payable onlyOwner returns (uint256) { .................... uint256 wstEthAmount = wstEthBalancePost - wstEthBalancePre; return (wstEthAmount); }
A more consistent design style should be used in all scenarios this is needed in the codebase.
The variable containing the result of the pre and post balance operation in both Wst.Eth and Reth.sol should both maintain same name for code consistency as they perform same action, the current names make it seem that one performs a different operation from the other.
function deposit() external payable onlyOwner returns (uint256) { .................... uint256 wstEthAmount = wstEthBalancePost - wstEthBalancePre; ................... }
function deposit() external payable onlyOwner returns (uint256) { ................... uint256 rethMinted = rethBalance2 - rethBalance1; ................... }
There is no error here that returns on failure to deposit derivative into the SfrxEth.sol contract.
function deposit() external payable onlyOwner returns (uint256) { IFrxETHMinter frxETHMinterContract = IFrxETHMinter( FRX_ETH_MINTER_ADDRESS ); uint256 sfrxBalancePre = IERC20(SFRX_ETH_ADDRESS).balanceOf( address(this) ); frxETHMinterContract.submitAndDeposit{value: msg.value}(address(this)); uint256 sfrxBalancePost = IERC20(SFRX_ETH_ADDRESS).balanceOf( address(this) ); return sfrxBalancePost - sfrxBalancePre; }
unlike is available in the other derivatives
function deposit() external payable onlyOwner returns (uint256) { ......................... require(sent, "Failed to send Ether"); ......................... }
and
Reth.sol.https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L200
function deposit() external payable onlyOwner returns (uint256) { ...................... require(rethBalance2 > rethBalance1, "No rETH was minted"); ................... }
Although since this is a contract to contract call, and would revert on failure, there should be a message from this deriavative about the failure and the reason it failed, just like in the other two derivative contracts.
The same operation in both WstEth.sol and Reth.sol, returns two different error messages.
function deposit() external payable onlyOwner returns (uint256) { .................... require(sent, "Failed to send Ether"); .................... }
function deposit() external payable onlyOwner returns (uint256) { .................. require(rethBalance2 > rethBalance1, "No rETH was minted"); ................. }
the former returns "Failed to send Ether" while the later returns "no rEth was minted". There should be consistency in the error messages, so as not to confuse the user and for better UX. Consider using the same error messages for these operations.
The adjustWeight function in SafEth.sol does not validate the _derivativeIndex parameter that is passed to it,
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L165
function adjustWeight( uint256 _derivativeIndex, uint256 _weight ) external onlyOwner { weights[_derivativeIndex] = _weight; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight; emit WeightChange(_derivativeIndex, _weight); }
there should be a check like so
function adjustWeight( uint256 _derivativeIndex, uint256 _weight ) external onlyOwner { require (_derivativeIndex < derivativeCount, "message"); ....................... }
this way a wrong index that exceeds the already supplied indexes is not included, for example if a wrong index of 7 is supplied when there is only 3 derivatives in this contract, it will mean that the new derivative would never be reached when looping for actions that take weights into consideration like so
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L191
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L84
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L171
#0 - c4-sponsor
2023-04-10T16:42:25Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-24T17:31:56Z
Picodes marked the issue as grade-b