Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 158/169
Findings: 1
Award: $35.48
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
initialize()
function can be called anybodyinitialize()
function can be called anybody when the contract is not initialized. Therefore it can be front-runned.
The attacker can initialize the contract before the legitimate deployer, hoping that the victim continues to use the same contract.
In the best case for the victim, they notice it and have to redeploy their contract costing gas.
More importantly, if someone else runs this function, they will have full authority because of the __Ownable_init()
function.
initialize()
function not mentioned to be called by factory:
Mentioned to be called by factory:
Add a control that makes initialize()
only call by the Deployer Contract;
+ if (msg.sender != DEPLOYER_ADDRESS) {revert NotDeployer();}
Use the constructor to initialize non-proxied contracts.
For initializing proxy contracts deploy contracts using a factory contract that immediately calls initialize after deployment or make sure to call it immediately after deployment and verify the transaction succeeded.
__gap[50]
storage variable to allow for new storage variables in later versionsFor upgradeable contracts, there must be storage gap to "allow developers to freely add new state variables in the future without compromising the storage compatibility with existing deployments" (quote OpenZeppelin). Otherwise it may be very difficult to write new implementation code. Without storage gap, the variable in child contract might be overwritten by the upgraded base contract if new variables are added to the base contract. This could have unintended and very serious consequences to the child contracts, potentially causing loss of user fund or cause the contract to malfunction completely. See.
For a description of this storage variable. While some contracts may not currently be sub-classed, adding the variable now protects against forgetting to add it in the future.
In the following context of the upgradeable contracts they are expected to use gaps for avoiding collision:
ERC20Upgradeable
ERC4626Upgradeable
OwnedUpgradeable
PausableUpgradeable
ReentrancyGuardUpgradeable
However, none of these contracts contain storage gap. The storage gap is essential for upgradeable contract because "It allows us to freely add new state variables in the future without compromising the storage compatibility with existing deployments". Refer to the bottom part of this article.
If a contract inheriting from a base contract contains additional variable, then the base contract cannot be upgraded to include any additional variable, because it would overwrite the variable declared in its child contract. This greatly limits contract upgradeability.
MultiRewardStaking.sol#L26 contract MultiRewardStaking is ERC4626Upgradeable, OwnedUpgradeable {
Vault.sol#L27 ERC20Upgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, OwnedUpgradeable
AdapterBase.sol#L28 ERC4626Upgradeable, PausableUpgradeable, OwnedUpgradeable, ReentrancyGuardUpgradeable,
Manual analysis
Recommend adding appropriate storage gap at the end of upgradeable contracts such as the below.
uint256[50] private __gap;
Reference OpenZeppelin upgradeable contract templates.
Return values not being checked may lead into unexpected behaviors with functions. Not events/Error are being emitted if that fails, so functions would be called even of not being working as expect.
deposit
redeem
withdraw
rejected
Check values and revert
/emit
events if needed
Vault should inherit from IPausable
Use of 1e18
, 1e17
, 2e17
... can be declared as constants
MultiRewardEscrow.sol#L108 uint256 fee = Math.mulDiv(amount, feePerc, 1e18);
MultiRewardEscrow.sol#L211 if (tokenFees[i] >= 1e17) revert DontGetGreedy(tokenFees[i]);
MultiRewardStaking.sol#L197 uint256 escrowed = rewardAmount.mulDiv(uint256(escrowInfo.escrowPercentage), 1e18, Math.Rounding.Down);
Vault.sol#L144 assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down)
Vault.sol#L183 1e18 - depositFee,
Vault.sol#L224 1e18 - withdrawalFee,
Vault.sol#L265 1e18,
Vault.sol#L330 assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down)
Vault.sol#L345 1e18 - depositFee,
Vault.sol#L367 1e18 - withdrawalFee,
Vault.sol#L389 1e18,
Vault.sol#L437 ) / 1e18
Vault.sol#L449 uint256 shareValue = convertToAssets(1e18);
Vault.sol#L456 1e36,
Vault.sol#L466 uint256 public highWaterMark = 1e18;
Vault.sol#L484 uint256 shareValue = convertToAssets(1e18);
Vault.sol#L498 highWaterMark = convertToAssets(1e18);
Vault.sol#L527 newFees.deposit >= 1e18 ||
Vault.sol#L528 newFees.withdrawal >= 1e18 ||
Vault.sol#L529 newFees.management >= 1e18 ||
Vault.sol#L530 newFees.performance >= 1e18
AdapterBase.sol#L85 highWaterMark = 1e18;
AdapterBase.sol#L531 uint256 shareValue = convertToAssets(1e18);
AdapterBase.sol#L538 1e36,
AdapterBase.sol#L565 uint256 shareValue = convertToAssets(1e18);
1e18
) rather than exponentiation (e.g. 10 ** 18
)YearnAdapter.sol#L25 uint256 constant DEGRADATION_COEFFICIENT = 10**18;
MultiRewardStaking.sol#L274 uint64 ONE = (10**IERC20Metadata(address(rewardToken)).decimals()).safeCastTo64();
MultiRewardStaking.sol#L406 deltaIndex = accrued.mulDiv(uint256(10**decimals()), supplyTokens, Math.Rounding.Down).safeCastTo224();
Consider replacing 10 ** value
notation to scientific notation
Some lines use // x
and some use //x
. Following the common style comment should be as follows:
-uint256 public genericDeclaration; //generic comment without space` +uint256 public genericDeclaration; // generic comment with space`
This is done in other fragments of the code but not here
-VaultController.sol#L751-L758
-VaultController.sol#L791-L798
Add code like this:
if (_newCoolAddress == newCoolAddress) revert ADDRESS_SAME();
Usually lines in source code are limited to 80 characters. Today’s screens are much larger so it’s reasonable to stretch this in some cases. Solidity newer guidelines suggest 120 characters. Reference: Long lines should be wrapped to conform with Solidity Style guidelines
Following lines with more than 120:
#0 - c4-judge
2023-02-28T18:18:58Z
dmvt marked the issue as grade-b