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: 122/246
Findings: 2
Award: $24.26
🌟 Selected for report: 0
🚀 Solo Findings: 0
11.1318 USDC - $11.13
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L48-L56
Staking is not paused in initialize function or constructor which will cause loss of funds for anyone who calls SafEth.stake() before any derivatives are added
constructor() { _disableInitializers(); } /* snippet */ function initialize( string memory _tokenName, string memory _tokenSymbol ) external initializer { ERC20Upgradeable.__ERC20_init(_tokenName, _tokenSymbol); _transferOwnership(msg.sender); minAmount = 5 * 10 ** 17; // initializing with .5 ETH as minimum maxAmount = 200 * 10 ** 18; // initializing with 200 ETH as maximum }
stake function will mint 0 amount for any calls that are done for derivativeCount == 0:
function stake() external payable { require(pauseStaking == false, "staking is paused"); require(msg.value >= minAmount, "amount too low"); require(msg.value <= maxAmount, "amount too high"); uint256 underlyingValue = 0; // Getting underlying value in terms of ETH for each derivative for (uint i = 0; i < derivativeCount; i++) // L 71 underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18; uint256 totalSupply = totalSupply(); uint256 preDepositPrice; // Price of safETH in regards to ETH if (totalSupply == 0) preDepositPrice = 10 ** 18; // initializes with a price of 1 else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply; uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system for (uint i = 0; i < derivativeCount; i++) { // L 84 uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; if (weight == 0) continue; uint256 ethAmount = (msg.value * weight) / totalWeight; // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; totalStakeValueEth += derivativeReceivedEthValue; } // mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; _mint(msg.sender, mintAmount); emit Staked(msg.sender, msg.value, mintAmount); }
1. Alice wants to stake ETH right after initialize function has been called. 2. Alice calls stake with any msg.value 3. derivativeCount is still 0 because derivatives have not been added yet (https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEthStorage.sol#L18) 4. both for loops will be skipped (L 71 and 84) 5. mintAmount will be 0 which means Alice has no ability to get her funds back via calling unstake()
Manual Review
make sure mintAmount != 0 or set derivatives in initialize-function or pause staking in the initialize-function until all derivatives are added
#0 - c4-pre-sort
2023-04-04T19:33:38Z
0xSorryNotSorry marked the issue as duplicate of #363
#1 - c4-judge
2023-04-21T16:29:01Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-04-21T16:31:52Z
Picodes marked the issue as satisfactory
🌟 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
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L18
contract SafEth is Initializable, ERC20Upgradeable, OwnableUpgradeable, SafEthStorage {
(note that derivatives have been excluded even though they inherit from the same OwnableUpgradeable contract, since their owner should always be SafEth thus preventing _transferOwnership being called)
Inheriting from OpenZeppelin's OwnableUpgradeable contract means you are using a single-step ownership transfer pattern. If an admin provides an incorrect address for the new owner this will result in none of the onlyOwner marked methods being callable again. The better way to do this is to use a two-step ownership transfer approach, where the new owner should first claim its new rights before they are transferred.
Context: All contracts
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
Context: All derivative contracts
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L42-L45 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L36-L39 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L33-L36
function initialize(address _owner) external initializer { _transferOwnership(_owner); maxSlippage = (1 * 10 ** 16); // 1% }
function setMinAmount(uint256 _minAmount) external onlyOwner { minAmount = _minAmount; emit ChangeMinAmount(minAmount); }
function setMaxAmount(uint256 _maxAmount) external onlyOwner { maxAmount = _maxAmount; emit ChangeMaxAmount(maxAmount); }
function setMaxSlippage( uint _derivativeIndex, uint _slippage ) external onlyOwner { derivatives[_derivativeIndex].setMaxSlippage(_slippage); emit SetMaxSlippage(_derivativeIndex, _slippage); }
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L158
@notice - Adds new derivative to the index fund @dev - Weights are only in regards to each other, total weight changes with this function @dev - If you want exact weights either do the math off chain or reset all existing derivates to the weights you want @dev - Weights are approximate as it will slowly change as people stake @param _derivativeIndex - index of the derivative you want to update the weight @param _weight - new weight for this derivative. */ 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); }
@notice is wrong, the adjustWeight() function does not add a new derivative
there is no check preventing derivatives from being added multiple times and no function that allows for full removal of a derivative: https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L182-L195
function addDerivative( address _contractAddress, uint256 _weight ) external onlyOwner { derivatives[derivativeCount] = IDerivative(_contractAddress); weights[derivativeCount] = _weight; derivativeCount++; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight; emit DerivativeAdded(_contractAddress, _weight, derivativeCount); }
function addDerivative( address _contractAddress, uint256 _weight ) external onlyOwner { derivatives[derivativeCount] = IDerivative(_contractAddress); weights[derivativeCount] = _weight; derivativeCount++; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight; emit DerivativeAdded(_contractAddress, _weight, derivativeCount); }
functions that iterate over derivativeCount: rebalanceToWeights():
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(); }
stake():
function stake() external payable { require(pauseStaking == false, "staking is paused"); require(msg.value >= minAmount, "amount too low"); require(msg.value <= maxAmount, "amount too high"); uint256 underlyingValue = 0; // Getting underlying value in terms of ETH for each derivative for (uint i = 0; i < derivativeCount; i++) // @audit what happens if someone stakes before the there have been any derivatives added? underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * // @audit ethPerDerivative() for wsteth and sfrxeth does not account for the passed function parameter derivatives[i].balance()) / 10 ** 18; uint256 totalSupply = totalSupply(); uint256 preDepositPrice; // Price of safETH in regards to ETH if (totalSupply == 0) preDepositPrice = 10 ** 18; // initializes with a price of 1 else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply; uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; if (weight == 0) continue; uint256 ethAmount = (msg.value * weight) / totalWeight; // @audit totalweight is initialized to 0 so this will revert if totalWeight is not set right? // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; totalStakeValueEth += derivativeReceivedEthValue; } // mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; _mint(msg.sender, mintAmount); emit Staked(msg.sender, msg.value, mintAmount); }
unstake():
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); }
adjustWeight():
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); }
Style recommendations: It is recommended to follow certain style guidelines such as: external below public functions
public functions below external functions: https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L122 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L111 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L86 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L93
external/public below private: https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L156 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L211 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L221
slippage should be decided by the user not the protocol, in times of market volatility it could be that users want to withdraw yet are unable due to slippage set by the owner. For a more user friendly experience I recommend letting the user decide the slippage
#0 - c4-sponsor
2023-04-07T21:56:56Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-24T17:19:20Z
Picodes marked the issue as grade-b