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: 43/169
Findings: 3
Award: $384.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: csanuragjain
Also found by: 0xNazgul, 0xNineDec, 0xSmartContract, 0xdeadbeef0x, Bauer, Deivitto, Josiah, KIntern_NA, RaymondFam, Rolezn, UdarTeam, Viktor_Cortess, btk, joestakey, koxuan, pavankv, rbserver, rvi0x
9.1667 USDC - $9.17
Judge has assessed an item in Issue #356 as 2 risk. The relevant finding follows:
[L-01] ERC4626 does not work with fee-on-transfer tokens in project Impact ERC20 token contract can be deposited with the deposit function.
With the following part of the code, the ERC20 transfer from msg.sender to the contract takes place; asset.safeTransferFrom(msg.sender, address(this), assets);
src/vault/Vault.sol: 134: function deposit(uint256 assets, address receiver) 135: public 136: nonReentrant 137: whenNotPaused 138: syncFeeCheckpoint 139: returns (uint256 shares) 140: { 141: if (receiver == address(0)) revert InvalidReceiver(); 142: 143: uint256 feeShares = convertToShares( 144: assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down) 145: ); 146: 147: shares = convertToShares(assets) - feeShares; 148: 149: if (feeShares > 0) _mint(feeRecipient, feeShares); 150: 151: _mint(receiver, shares); 152: 153: asset.safeTransferFrom(msg.sender, address(this), assets); 154: 155: adapter.deposit(assets, address(this)); 156: 157: emit Deposit(msg.sender, receiver, assets, shares); 158: } Proof Of Concept Contract calls transfer from contractA 100 tokens to current contract Current contract thinks it received 100 tokens It updates tokenBalances sheet +100 tokens While actually contract received only 90 tokens That breaks whole math for given token
Recommended Mitigation Steps There are several possible scenarios to prevent that.
Check how much contract balance is increased/decreased after every transfer (costs more gas) Make a separate contract that checks if the token has fee-on-transfer and store bool value depending on the result. If there is fee-on-transfer you can throw a require not allowing to use such token in the system while still saving lots of gas checking it only once. Or if you still want to allow fee-on-transfer tokens, amount variable must be updated to the balance difference after and before transfer.
This code block should be used if you want to ban fee-on-transfer tokens;
uint256 balanceBefore = IERC20(token).balanceOf(address(this)); IERC20(token). safeTransferFrom(msg.sender, address(this), amount); uint256 balanceAfter = IERC20(token).balanceOf(address(this)); require(balanceAfter - amount == balanceBefore, "feeOnTransfer token not supported");
#0 - c4-judge
2023-03-01T01:17:32Z
dmvt marked the issue as duplicate of #503
#1 - c4-judge
2023-03-01T01:30:56Z
dmvt marked the issue as satisfactory
🌟 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
305.798 USDC - $305.80
Number | Issues Details | Context |
---|---|---|
[L-01] | ERC4626 does not work with fee-on-transfer tokens in project | |
[L-02] | Use ERC-5143: Slippage Protection for Tokenized Vault | |
[L-03] | initialize() function can be called by anybody | 1 |
[L-04] | Insufficient coverage | All Contracts |
[L-05] | Missing Event for initialize | 14 |
[L-06] | Project Upgrade and Stop Scenario should be | |
[L-07] | Loss of precision due to rounding in amount / uint256(rewardsPerSecond) | 1 |
[L-08] | Use Fuzzing Test for complicated code bases | |
[L-09] | Add to Event-Emit for critical function | 1 |
[L-10] | The Timestamp variable is limited to work until 2106 | 1 |
[L-11] | Signature Malleability of EVM's ecrecover() | 3 |
[L-12] | Update codes to avoid Compile Errors | 15 |
[L-13] | _withdraw event is missing parameters | 1 |
[L-14] | Critical Address Changes Should Use Two-step Procedure | 1 |
[L-15] | Lack of Input Validation | 1 |
[L-16] | Prevent division by 0 | 1 |
[L-17] | Although vaults.length length input value is up to uint256 , this value is truncated to uint8 | 1 |
Total 17 issues
Number | Issues Details | Context |
---|---|---|
[N-01] | Omissions in Events | 1 |
[N-02] | NatSpec comments should be increased in contracts | All Contracts |
[N-03] | You should leave a space after the word "Popcorn Yearn" when concatenating with string.concat | 1 |
[N-04] | Function writing that does not comply with the Solidity Style Guide | All Contracts |
[N-05] | Include return parameters in NatSpec comments | All Contracts |
[N-06] | Tokens accidentally sent to the contract cannot be recovered | 1 |
[N-07] | Take advantage of Custom Error's return value property | 22 |
[N-08] | Repeated validation logic can be converted into a function modifier | 5 |
[N-09] | Use a more recent version of Solidity | 35 |
[N-10] | For functions, follow Solidity standard naming conventions (internal function style rule) | 4 |
[N-11] | Floating pragma | 35 |
[N-12] | Use descriptive names for Contracts and Libraries | All Contracts |
[N-13] | Use SMTChecker | |
[N-14] | Add NatSpec Mapping comment | 8 |
[N-15] | Showing the actual values of numbers in NatSpec comments makes checking and reading code easier | 6 |
[N-16] | Lines are too long | 7 |
[N-17] | Open TODOs | 1 |
[N-18] | Constants on the left are better | 9 |
[N-19] | Confusing event-emit argument names | 4 |
[N-20] | constants should be defined rather than using magic numbers | 1 |
[N-21] | Missing timelock for critical parameter change | 1 |
[N-22] | Use the delete keyword instead of assigning a value of 0 | 2 |
[N-23] | Variable names that consist of all capital letters should be reserved for only constant/immutable variables | 6 |
[N-24] | Avoid shadowing inherited state variables | 1 |
[N-25] | Not using the named return variables anywhere in the function is confusing | 1 |
[N-26] | Unnecessary Emission of block.timestamp | 2 |
[N-27] | Function Calls in Loop Could Lead to Denial of Service due to Array length not being checked | 1 |
[N-28] | Don't use _msgSender() if not supporting EIP-2771 | 4 |
Total 28 issues
ERC20 token contract can be deposited with the deposit
function.
With the following part of the code, the ERC20 transfer from msg.sender to the contract takes place;
asset.safeTransferFrom(msg.sender, address(this), assets);
src/vault/Vault.sol: 134: function deposit(uint256 assets, address receiver) 135: public 136: nonReentrant 137: whenNotPaused 138: syncFeeCheckpoint 139: returns (uint256 shares) 140: { 141: if (receiver == address(0)) revert InvalidReceiver(); 142: 143: uint256 feeShares = convertToShares( 144: assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down) 145: ); 146: 147: shares = convertToShares(assets) - feeShares; 148: 149: if (feeShares > 0) _mint(feeRecipient, feeShares); 150: 151: _mint(receiver, shares); 152: 153: asset.safeTransferFrom(msg.sender, address(this), assets); 154: 155: adapter.deposit(assets, address(this)); 156: 157: emit Deposit(msg.sender, receiver, assets, shares); 158: }
Contract calls transfer from contractA 100 tokens to current contract
Current contract thinks it received 100 tokens
It updates tokenBalances
sheet +100 tokens
While actually contract received only 90 tokens
That breaks whole math for given token
There are several possible scenarios to prevent that.
Check how much contract balance is increased/decreased after every transfer (costs more gas) Make a separate contract that checks if the token has fee-on-transfer and store bool value depending on the result. If there is fee-on-transfer you can throw a require not allowing to use such token in the system while still saving lots of gas checking it only once. Or if you still want to allow fee-on-transfer tokens, amount variable must be updated to the balance difference after and before transfer.
This code block should be used if you want to ban fee-on-transfer tokens;
uint256 balanceBefore = IERC20(token).balanceOf(address(this)); IERC20(token). safeTransferFrom(msg.sender, address(this), amount); uint256 balanceAfter = IERC20(token).balanceOf(address(this)); require(balanceAfter - amount == balanceBefore, "feeOnTransfer token not supported");
Project use ERC-4626 standart
EIP-4626 is vulnerable to the so-called inflation attacks. This attack results from the possibility to manipulate the exchange rate and front run a victim’s deposit when the vault has low liquidity volume.
Proposed mitigation
The following standard extends the EIP-4626 Tokenized Vault standard with functions dedicated to the safe interaction between EOAs and the vault when price is subject to slippage.
https://eips.ethereum.org/EIPS/eip-5143
initialize()
function can be called anybody when the contract is not initialized.
More importantly, if someone else runs this function, they will have full authority because of the __Ownable_init()
function.
Here is a definition of initialize()
function.
src/utils/MultiRewardStaking.sol: 41: function initialize( 42: IERC20 _stakingToken, 43: IMultiRewardEscrow _escrow, 44: address _owner 45: ) external initializer { 46: __ERC4626_init(IERC20Metadata(address(_stakingToken))); 47: __Owned_init(_owner); 48: 49: _name = string(abi.encodePacked("Staked ", IERC20Metadata(address(_stakingToken)).name())); 50: _symbol = string(abi.encodePacked("pst-", IERC20Metadata(address(_stakingToken)).symbol())); 51: _decimals = IERC20Metadata(address(_stakingToken)).decimals(); 52: 53: escrow = _escrow; 54: 55: INITIAL_CHAIN_ID = block.chainid; 56: INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); 57: }
Add a control that makes initialize()
only call the Deployer Contract or EOA;
if (msg.sender != DEPLOYER_ADDRESS) { revert NotDeployer(); }
Description: The test coverage rate of the project is ~85%. Testing all functions is best practice in terms of security criteria. Due to its capacity, test coverage is expected to be 100%.
| File | % Lines | % Statements | % Branches | % Funcs | |-------------------------------------------------------------------------------------|-------------------|--------------------|------------------|------------------| | src/utils/EIP165.sol | 100.00% (0/0) | 100.00% (0/0) | 100.00% (0/0) | 0.00% (0/1) | | src/utils/MultiRewardEscrow.sol | 100.00% (42/42) | 88.33% (53/60) | 55.56% (10/18) | 100.00% (9/9) | | src/utils/MultiRewardStaking.sol | 100.00% (110/110) | 89.78% (123/137) | 63.04% (29/46) | 100.00% (26/26) | | src/vault/AdminProxy.sol | 100.00% (1/1) | 100.00% (1/1) | 100.00% (0/0) | 100.00% (1/1) | | src/vault/CloneFactory.sol | 100.00% (5/5) | 100.00% (6/6) | 100.00% (4/4) | 100.00% (1/1) | | src/vault/CloneRegistry.sol | 66.67% (4/6) | 66.67% (4/6) | 100.00% (0/0) | 33.33% (1/3) | | src/vault/DeploymentController.sol | 100.00% (13/13) | 93.33% (14/15) | 50.00% (1/2) | 100.00% (6/6) | | src/vault/PermissionRegistry.sol | 100.00% (8/8) | 83.33% (10/12) | 50.00% (2/4) | 100.00% (3/3) | | src/vault/TemplateRegistry.sol | 100.00% (18/18) | 80.00% (16/20) | 50.00% (4/8) | 100.00% (6/6) | | src/vault/Vault.sol | 90.00% (108/120) | 84.83% (123/145) | 57.14% (24/42) | 88.57% (31/35) | | src/vault/VaultController.sol | 98.87% (175/177) | 88.21% (247/280) | 52.56% (41/78) | 100.00% (42/42) | | src/vault/VaultRegistry.sol | 60.00% (6/10) | 63.64% (7/11) | 100.00% (2/2) | 33.33% (2/6) | | src/vault/adapter/abstracts/AdapterBase.sol | 95.74% (90/94) | 91.96% (103/112) | 70.83% (17/24) | 76.32% (29/38) | | src/vault/adapter/abstracts/WithRewards.sol | 0.00% (0/1) | 0.00% (0/1) | 100.00% (0/0) | 0.00% (0/3) | | src/vault/adapter/beefy/BeefyAdapter.sol | 80.85% (38/47) | 79.31% (46/58) | 55.00% (11/20) | 69.23% (9/13) | | src/vault/adapter/yearn/YearnAdapter.sol | 92.86% (26/28) | 86.11% (31/36) | 50.00% (4/8) | 92.31% (12/13) |
Context:
14 results src/utils/MultiRewardStaking.sol: 41: function initialize( src/vault/Vault.sol: 57: function initialize( src/vault/adapter/beefy/BeefyAdapter.sol: 46: function initialize( src/vault/adapter/yearn/YearnAdapter.sol: 34: function initialize( src/utils/MultiRewardEscrow.sol: 30: constructor(address _owner, address _feeRecipient) Owned(_owner) { src/vault/AdminProxy.sol: 16: constructor(address _owner) Owned(_owner) {} src/vault/CloneFactory.sol: 23: constructor(address _owner) Owned(_owner) {} src/vault/CloneRegistry.sol: 22: constructor(address _owner) Owned(_owner) {} src/vault/DeploymentController.sol: 35: constructor( src/vault/PermissionRegistry.sol: 20: constructor(address _owner) Owned(_owner) {} src/vault/TemplateRegistry.sol: 24: constructor(address _owner) Owned(_owner) {} src/vault/VaultController.sol: 45: * @notice Constructor of this contract. 53: constructor( src/vault/VaultRegistry.sol: 21: constructor(address _owner) Owned(_owner) {}
Description: Events help non-contract tools to track changes, and events prevent users from being surprised by changes Issuing event-emit during initialization is a detail that many projects skip
Recommendation: Add Event-Emit
At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an " EMERGENCY STOP (CIRCUIT BREAKER) PATTERN ".
https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol
amount / uint256(rewardsPerSecond)
Add scalar so roundings are negligible
src/vault/adapter/yearn/YearnAdapter.sol: 25: uint256 constant DEGRADATION_COEFFICIENT = 10**18; 122: ((lockedFundsRatio * lockedProfit) / DEGRADATION_COEFFICIENT);
src/vault/adapter/yearn/YearnAdapter.sol: 25: uint256 constant DEGRADATION_COEFFICIENT = 10**18; 114: function _calculateLockedProfit() internal view returns (uint256) { 115: uint256 lockedFundsRatio = (block.timestamp - yVault.lastReport()) * 116: yVault.lockedProfitDegradation(); 117: 118: if (lockedFundsRatio < DEGRADATION_COEFFICIENT) { 119: uint256 lockedProfit = yVault.lockedProfit(); 120: return 121: lockedProfit - 122: ((lockedFundsRatio * lockedProfit) / DEGRADATION_COEFFICIENT); 123: } else { 124: return 0; 125: } 126: }
I recommend fuzzing testing in complex code structures, especially PopCorn, where there is an innovative code base and a lot of computation.
Recommendation:
Use should fuzzing test like Echidna.
As Alberto Cuesta Canada said:
Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.
https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05
src/vault/AdminProxy.sol: 18 /// @notice Execute arbitrary management functions. 19: function execute(address target, bytes calldata callData) 20: external 21: onlyOwner 22: returns (bool success, bytes memory returndata) 23: { 24: return target.call(callData); 25: }
Timestamp
variable is limited to work until 2106limiting the timestamp to fit in a uint32 will cause the call below to start reverting in 2106
src/utils/MultiRewardStaking.sol: 220: event RewardInfoUpdate(IERC20 rewardToken, uint160 rewardsPerSecond, uint32 rewardsEndTimestamp); 275: uint32 rewardsEndTimestamp = rewardsPerSecond == 0 308: uint32 rewardsEndTimestamp = _calcRewardsEnd( 340: uint32 rewardsEndTimestamp = rewards.rewardsEndTimestamp; 352: uint32 rewardsEndTimestamp
Context:
3 results - 3 files src/utils/MultiRewardStaking.sol: 458 unchecked { 459: address recoveredAddress = ecrecover( 460 keccak256( src/vault/Vault.sol: 677 unchecked { 678: address recoveredAddress = ecrecover( 679 keccak256( src/vault/adapter/abstracts/AdapterBase.sol: 645 unchecked { 646: address recoveredAddress = ecrecover( 647 keccak256(
Description: Description: The function calls the Solidity ecrecover() function directly to verify the given signatures. However, the ecrecover() EVM opcode allows malleable (non-unique) signatures and thus is susceptible to replay attacks.
Although a replay attack seems not possible for this contract, I recommend using the battle-tested OpenZeppelin's ECDSA library.
Recommendation:* Use the ecrecover function from OpenZeppelin's ECDSA library for signature verification. (Ensure using a version > 4.7.3 for there was a critical bug >= 4.1.0 < 4.7.3).
Compiler run successful (with warnings) warning[2519]: Warning: This declaration shadows an existing declaration. --> src/vault/adapter/abstracts/AdapterBase.sol:388:9: | 388 | uint256 _totalAssets = totalAssets(); | ^^^^^^^^^^^^^^^^^^^^ Note: The shadowed declaration is here: --> src/vault/adapter/abstracts/AdapterBase.sol:258:5: | 258 | function _totalAssets() internal view virtual returns (uint256) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning[2519]: Warning: This declaration shadows an existing declaration. --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:159:12: | 159 | for (uint256 i; i < 3; ++i) { | ^^^^^^^^^ Note: The shadowed declaration is here: --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:155:10: | 155 | for (uint256 i; i < len; i++) { | ^^^^^^^^^ warning[2519]: Warning: This declaration shadows an existing declaration. --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:166:12: | 166 | for (uint256 i; i < 2; ++i) { | ^^^^^^^^^ Note: The shadowed declaration is here: --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:155:10: | 155 | for (uint256 i; i < len; i++) { | ^^^^^^^^^ warning[2519]: Warning: This declaration shadows an existing declaration. --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:214:12: | 214 | for (uint256 i; i < 3; ++i) { | ^^^^^^^^^ Note: The shadowed declaration is here: --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:210:10: | 210 | for (uint256 i; i < len; i++) { | ^^^^^^^^^ warning[2519]: Warning: This declaration shadows an existing declaration. --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:221:12: | 221 | for (uint256 i; i < 2; ++i) { | ^^^^^^^^^ Note: The shadowed declaration is here: --> test/vault/integration/abstract/AbstractVaultIntegrationTest.sol:210:10: | 210 | for (uint256 i; i < len; i++) { | ^^^^^^^^^ warning[9302]: Warning: Return value of low-level calls not used. --> src/vault/adapter/abstracts/AdapterBase.sol:444:13: | 444 | address(strategy).delegatecall( | ^ (Relevant source part starts here and spans across multiple lines). warning[2072]: Warning: Unused local variable. --> src/vault/Vault.sol:448:9: | 448 | uint256 highWaterMark_ = highWaterMark; | ^^^^^^^^^^^^^^^^^^^^^^ warning[2072]: Warning: Unused local variable. --> test/vault/integration/abstract/AbstractAdapterTest.sol:123:5: | 123 | uint256 callTime = block.timestamp; | ^^^^^^^^^^^^^^^^ warning[2072]: Warning: Unused local variable. --> test/vault/integration/abstract/AbstractAdapterTest.sol:475:5: | 475 | uint256 hwm = 1e18; | ^^^^^^^^^^^ warning[2072]: Warning: Unused local variable. --> test/vault/integration/abstract/AbstractAdapterTest.sol:481:5: | 481 | uint256 oldTotalAssets = adapter.totalAssets(); | ^^^^^^^^^^^^^^^^^^^^^^
_withdraw
event is missing parametersThe MultiRewardStaking.sol
contract has very important function; _withdraw
However, only amounts are published in emits, whereas msg.sender must be specified in every transaction, a contract or web page listening to events cannot react to users, emit does not serve the purpose. Basically, this event cannot be used
src/utils/MultiRewardStaking.sol: 120 /// @notice Internal withdraw function used by `withdraw()` and `redeem()`. Accrues rewards for the `caller` and `receiver`. 121: function _withdraw( 122: address caller, 123: address receiver, 124: address owner, 125: uint256 assets, 126: uint256 shares 127: ) internal override accrueRewards(caller, receiver) { 128: if (caller != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); 129: 130: _burn(owner, shares); 131: IERC20(asset()).safeTransfer(receiver, assets); 132: - 133: emit Withdraw(caller, receiver, owner, assets, shares); + 133: emit Withdraw(msg.sender, caller, receiver, owner, assets, shares); 134: }
add msg.sender
parameter in event-emit
The critical procedures should be two step process.
src/vault/Vault.sol: 552 */ 553: function setFeeRecipient(address _feeRecipient) external onlyOwner { 554: if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); 555: 556: emit FeeRecipientUpdated(feeRecipient, _feeRecipient); 557: 558: feeRecipient = _feeRecipient; 559: }
Recommended Mitigation Steps Lack of two-step procedure for critical operations leaves them error-prone. Consider adding two step procedure on the critical functions.
For defence-in-depth purpose, it is recommended to perform additional validation against the amount that the user is attempting to deposit, mint, withdraw and redeem to ensure that the submitted amount is valid.
OpenZeppelinTokenizedVault.sol#L9
src/vault/Vault.sol: 169 */ 170: function mint(uint256 shares, address receiver) 171: public 172: nonReentrant 173: whenNotPaused 174: syncFeeCheckpoint 175: returns (uint256 assets) 176: { 177: if (receiver == address(0)) revert InvalidReceiver(); + require(shares <= maxMint(receiver), "mint more than max"); 178: 179: uint256 depositFee = uint256(fees.deposit); 180: 181: uint256 feeShares = shares.mulDiv( 182: depositFee, 183: 1e18 - depositFee, 184: Math.Rounding.Down 185: ); 186: 187: assets = convertToAssets(shares + feeShares); 188: 189: if (feeShares > 0) _mint(feeRecipient, feeShares); 190: 191: _mint(receiver, shares); 192: 193: asset.safeTransferFrom(msg.sender, address(this), assets); 194: 195: adapter.deposit(assets, address(this)); 196: 197: emit Deposit(msg.sender, receiver, assets, shares); 198: }
On several locations in the code precautions are not being taken for not dividing by 0, this will revert the code. These functions can be calledd with 0 value in the input, this value is not checked for being bigger than 0, that means in some scenarios this can potentially trigger a division by zero.
rewardsPerSecond
can be assign to 0
src/utils/MultiRewardStaking.sol: 350 351: function _calcRewardsEnd( 352: uint32 rewardsEndTimestamp, 353: uint160 rewardsPerSecond, 354: uint256 amount 355: ) internal returns (uint32) { 356: if (rewardsEndTimestamp > block.timestamp) 357: amount += uint256(rewardsPerSecond) * (rewardsEndTimestamp - block.timestamp); 358: 359: return (block.timestamp + (amount / uint256(rewardsPerSecond))).safeCastTo32(); 360: }
vaults.length
length input value is up to uint256
, this value is truncated to uint8
Although vaults.length
length input value is up to uint256
, this value is truncated to uint8
If the user enters more vaults
parameter number than uint8 value, this will be truncated, instead a require control that checks more than uint8
value should be added, so that one entry above uint8 will not be possible.
src/vault/VaultController.sol: 334 */ 335: function changeVaultAdapters(address[] calldata vaults) external { 336: uint8 len = uint8(vaults.length); 337: for (uint8 i = 0; i < len; i++) { 338: (bool success, bytes memory returnData) = adminProxy.execute( 339: vaults[i], 340: abi.encodeWithSelector(IVault.changeAdapter.selector) 341: ); 342: if (!success) revert UnderlyingError(returnData); 343: } 344: }
Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters
The events should include the new value and old value where possible:
src/utils/MultiRewardEscrow.sol: 207: function setFees(IERC20[] memory tokens, uint256[] memory tokenFees) external onlyOwner { 208: if (tokens.length != tokenFees.length) revert ArraysNotMatching(tokens.length, tokenFees.length); 209: 210: for (uint256 i = 0; i < tokens.length; i++) { 211: if (tokenFees[i] >= 1e17) revert DontGetGreedy(tokenFees[i]); 212: 213: fees[tokens[i]].feePerc = tokenFees[i]; - 214: emit FeeSet(tokens[i], tokenFees[i]); + 214: emit FeeSet(tokens[i], tokenFeesold[i], tokenFeesNew[i]); 215: } 216: }
Context: All project have 226 functions (without interfaces) but they have only 95 NatSpec’s
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Recommendation: NatSpec comments should be increased in contracts
string.concat
src/vault/adapter/yearn/YearnAdapter.sol: 34: function initialize( 35: bytes memory adapterInitData, 36: address externalRegistry, 37: bytes memory 38: ) external initializer { 39: (address _asset, , , , , ) = abi.decode( 40: adapterInitData, 41: (address, address, address, uint256, bytes4[8], bytes) 42: ); 43: __AdapterBase_init(adapterInitData); 44: 45: yVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); 46: 47: _name = string.concat( - 48: "Popcorn Yearn", + 48: "Popcorn Yearn ", 49: IERC20Metadata(asset()).name(), 50: " Adapter" 51: ); 52: _symbol = string.concat("popY-", IERC20Metadata(asset()).symbol()); 53: 54: IERC20(_asset).approve(address(yVault), type(uint256).max); 55: } 56: 57 function name()
Function writing
that does not comply with the Solidity Style Guide
Context: All Contracts
Description: Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
constructor receive function (if exists) fallback function (if exists) external public internal private within a grouping, place the view and pure functions last
Context: All project have 118 returns variable (without interfaces) but they have only 20 @return NatSpec’s
Description: It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
Recommendation: Include return parameters in NatSpec comments
Recommendation Code Style: (from Uniswap3)
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1);
Using balanceOf for reward distribution means accidentally sent reward tokens to the contract will be distributed to all users who are eligible for rewards.
Examine and consider the trade-offs of using internal accounting instead of balanceOf(address(this) . Implement functions that allow to withdraw the accidentally sent tokens from the contracts.
Contex MultiRewardStaking.sol
It can't be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts.
Add this code:
/** * @notice Sends ERC20 tokens trapped in contract to external address * @dev Onlyowner is allowed to make this function call * @param account is the receiving address * @param externalToken is the token being sent * @param amount is the quantity being sent * @return boolean value indicating whether the operation succeeded. * */ function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) { IERC20(externalToken).transfer(account, amount); return true; } }
An important feature of Custom Error is that values such as address, tokenID, msg.value can
be written inside the ()
sign, this kind of approach provides a serious advantage in
debugging and examining the revert details of dapps such as tenderly.
22 results - 7 files src/utils/MultiRewardEscrow.sol: 95: if (token == IERC20(address(0))) revert ZeroAddress(); 96: if (account == address(0)) revert ZeroAddress(); 97: if (amount == 0) revert ZeroAmount(); 98: if (duration == 0) revert ZeroAmount(); src/vault/CloneFactory.sol: 45: if (!success) revert DeploymentInitFailed(); src/vault/PermissionRegistry.sol: 40: if (len != newPermissions.length) revert Mismatch(); 43: if (newPermissions[i].endorsed && newPermissions[i].rejected) revert Mismatch(); src/vault/Vault.sol: 74: if (address(asset_) == address(0)) revert InvalidAsset(); 75: if (address(asset_) != adapter_.asset()) revert InvalidAdapter(); 90: if (feeRecipient_ == address(0)) revert InvalidFeeRecipient(); 141: if (receiver == address(0)) revert InvalidReceiver(); 177: if (receiver == address(0)) revert InvalidReceiver(); 216: if (receiver == address(0)) revert InvalidReceiver(); 258: if (receiver == address(0)) revert InvalidReceiver(); 531: ) revert InvalidVaultFees(); 554: if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); 580: revert VaultAssetMismatchNewAdapterAsset(); 631: revert InvalidQuitPeriod(); src/vault/VaultController.sol: 695: if (!cloneRegistry.cloneExists(adapter)) revert AdapterConfigFaulty(); 701: if (length1 != length2) revert ArrayLengthMissmatch(); src/vault/VaultRegistry.sol: 45: if (metadata[_metadata.vault].vault != address(0)) revert VaultAlreadyRegistered(); src/vault/adapter/beefy/BeefyAdapter.sol: 222: if (address(beefyBooster) == address(0)) revert NoBeefyBooster();
If a query or logic is repeated over many lines, using a modifier improves the readability and reusability of the code
5 results src/utils/MultiRewardStaking.sol: 142: if (from == address(0) || to == address(0)) revert ZeroAddressTransfer(from, to); 481: if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress); src/vault/Vault.sol: 702: if (recoveredAddress == address(0) || recoveredAddress != owner) src/vault/VaultController.sol: 837: if (address(_deploymentController) == address(0) || address(deploymentController) == address(_deploymentController)) src/vault/adapter/abstracts/AdapterBase.sol: 670: if (recoveredAddress == address(0) || recoveredAddress != owner)
Context:
35 results - 35 files src/interfaces/IEIP165.sol: 1 // SPDX-License-Identifier: AGPL-3.0-only 2: pragma solidity ^0.8.15; 3 src/interfaces/IMultiRewardEscrow.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/IMultiRewardStaking.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IAdapter.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IAdminProxy.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/ICloneFactory.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/ICloneRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IDeploymentController.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IERC4626.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IPermissionRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IStrategy.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/ITemplateRegistry.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IVault.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IVaultController.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IVaultRegistry.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IWithRewards.sol: 3 4: pragma solidity ^0.8.15; 5 src/utils/EIP165.sol: 3 4: pragma solidity ^0.8.15; 5 src/utils/MultiRewardEscrow.sol: 3 4: pragma solidity ^0.8.15; 5 src/utils/MultiRewardStaking.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/AdminProxy.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/CloneFactory.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/CloneRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/DeploymentController.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/PermissionRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/TemplateRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/Vault.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/VaultController.sol: 2 3: pragma solidity ^0.8.15; 4 src/vault/VaultRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/abstracts/AdapterBase.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/abstracts/OnlyStrategy.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/abstracts/WithRewards.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/beefy/BeefyAdapter.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/beefy/IBeefy.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/yearn/IYearn.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/yearn/YearnAdapter.sol: 3 4: pragma solidity ^0.8.15;
Description: For security, it is best practice to use the latest Solidity version. For the security fix list in the versions; https://github.com/ethereum/solidity/blob/develop/Changelog.md
Recommendation:
Old version of Solidity is used (0.8.15)
, newer version can be used (0.8.17)
Context:
4 results - 4 files src/utils/MultiRewardEscrow.sol: 71: uint256 internal nonce; src/utils/MultiRewardStaking.sol: 491: function computeDomainSeparator() internal view virtual returns (bytes32) { src/vault/Vault.sol: 716: function computeDomainSeparator() internal view virtual returns (bytes32) { src/vault/adapter/abstracts/AdapterBase.sol: 684: function computeDomainSeparator() internal view virtual returns (bytes32) {
Description: The above codes don't follow Solidity's standard naming convention,
internal and private functions : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)
Description: 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. https://swcregistry.io/docs/SWC-103
Floating Pragma List:
35 results - 35 files src/interfaces/IEIP165.sol: 2: pragma solidity ^0.8.15; src/interfaces/IMultiRewardEscrow.sol: 3: pragma solidity ^0.8.15; 4 src/interfaces/IMultiRewardStaking.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IAdapter.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IAdminProxy.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/ICloneFactory.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/ICloneRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IDeploymentController.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IERC4626.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IPermissionRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IStrategy.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/ITemplateRegistry.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IVault.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IVaultController.sol: 3 4: pragma solidity ^0.8.15; 5 src/interfaces/vault/IVaultRegistry.sol: 2 3: pragma solidity ^0.8.15; 4 src/interfaces/vault/IWithRewards.sol: 3 4: pragma solidity ^0.8.15; 5 src/utils/EIP165.sol: 3 4: pragma solidity ^0.8.15; 5 src/utils/MultiRewardEscrow.sol: 3 4: pragma solidity ^0.8.15; 5 src/utils/MultiRewardStaking.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/AdminProxy.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/CloneFactory.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/CloneRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/DeploymentController.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/PermissionRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/TemplateRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/Vault.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/VaultController.sol: 2 3: pragma solidity ^0.8.15; 4 src/vault/VaultRegistry.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/abstracts/AdapterBase.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/abstracts/OnlyStrategy.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/abstracts/WithRewards.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/beefy/BeefyAdapter.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/beefy/IBeefy.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/yearn/IYearn.sol: 3 4: pragma solidity ^0.8.15; 5 src/vault/adapter/yearn/YearnAdapter.sol: 3 4: pragma solidity ^0.8.15;
Recommendation: Lock the pragma version and also consider known bugs (https://github.com/ethereum/solidity/releases) for the compiler version that is chosen.
This codebase will be difficult to navigate, as there are no descriptive naming conventions that specify which files should contain meaningful logic.
Prefixes should be added like this by filing:
Interface I_ absctract contracts Abs_ Libraries Lib_
We recommend that you implement this or a similar agreement.
The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.
https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19
Description: Add NatSpec comments describing mapping keys and values
Context:
8 results - 6 files src/vault/CloneRegistry.sol: 28: mapping(address => bool) public cloneExists; src/vault/PermissionRegistry.sol: 26: mapping(address => Permission) public permissions; src/vault/TemplateRegistry.sol: 32: mapping(bytes32 => bytes32[]) public templateIds; 33: mapping(bytes32 => bool) public templateExists; 35: mapping(bytes32 => bool) public templateCategoryExists; src/vault/Vault.sol: 659: mapping(address => uint256) public nonces; src/vault/VaultController.sol: 851: mapping(bytes32 => bytes32) public activeTemplateId; src/vault/adapter/abstracts/AdapterBase.sol: 627: mapping(address => uint256) public nonces;
Recommendation:
/// @dev address(user) -> address(ERC0 Contract Address) -> uint256(allowance amount from user) mapping(address => mapping(address => uint256)) public allowance;
6 results - 3 files src/vault/Vault.sol: - 35: uint256 constant SECONDS_PER_YEAR = 365.25 days; + 35: uint256 constant SECONDS_PER_YEAR = 365.25 days; // 31_536_000 31_557_600 (365.25*24*60*60) - 619: uint256 public quitPeriod = 3 days; + 619: uint256 public quitPeriod = 3 days; // 259_200 (3*24*60*60) - 630: if (_quitPeriod < 1 days || _quitPeriod > 7 days) + 630: if (_quitPeriod < 1 days || _quitPeriod > 7 days) // 604_800 (7*24*60*60) src/vault/VaultController.sol: - 793: if (newCooldown > 1 days) revert InvalidHarvestCooldown(newCooldown); + 793: if (newCooldown > 1 days) revert InvalidHarvestCooldown(newCooldown); // 86_400 (1*24*60*60) src/vault/adapter/abstracts/AdapterBase.sol: - 502: if (newCooldown >= 1 days) revert InvalidHarvestCooldown(newCooldown); + 502: if (newCooldown >= 1 days) revert InvalidHarvestCooldown(newCooldown); // 86_400 (1*24*60*60)
Description: It is generally recommended that lines in the source code should not exceed 80-120 characters. Today's screens are much larger, so in some cases it makes sense to expand that.The lines above should be split when they reach that length, as the files will most likely be on GitHub and GitHub always uses a scrollbar when the length is more than 164 characters.
(why-is-80-characters-the-standard-limit-for-code-width)[https://softwareengineering.stackexchange.com/questions/148677/why-is-80-characters-the-standard-limit-for-code-width]
Recommendation: Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html#introduction
thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 );
Context:
src/vault/adapter/abstracts/AdapterBase.sol: 515 516: // TODO use deterministic fee recipient proxy 517: address FEE_RECIPIENT = address(0x4444);
Recommendation: Use temporary TODOs as you work on a feature, but make sure to treat them before merging. Either add a link to a proper issue in your TODO, or remove it from the code.
If you use the constant first you support structures that veil programming errors. And one should only produce code either to add functionality, fix an programming error or trying to support structures to avoid programming errors (like design patterns).
https://www.moserware.com/2008/01/constants-on-left-are-better-but-this.html
9 results - 3 files src/utils/MultiRewardEscrow.sol: 97: if (amount == 0) revert ZeroAmount(); 98: if (duration == 0) revert ZeroAmount(); 160: if (claimable == 0) revert NotClaimable(escrowId); src/utils/MultiRewardStaking.sol: 174: if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); 258: if (rewardsPerSecond == 0) revert ZeroRewardsSpeed(); 299: if (rewardsPerSecond == 0) revert ZeroAmount(); 325: if (amount == 0) revert ZeroAmount(); 420: if (oldIndex == 0) { src/vault/adapter/yearn/YearnAdapter.sol: 90: if (yVault.totalSupply() == 0) return yShares;
In the naming of event-emit arguments; Naming can be confusing when using pre and current value
src/vault/Vault.sol: - 553: function setFeeRecipient(address _feeRecipient) external onlyOwner { + 553: function setFeeRecipient(address NewfeeRecipient) external onlyOwner { - 554: if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); + 554: if (NewfeeRecipient == address(0)) revert InvalidFeeRecipient(); 555: - 556: emit FeeRecipientUpdated(feeRecipient, _feeRecipient); + 556: emit FeeRecipientUpdated(oldfeeRecipient, NewfeeRecipient); 557: - 558: feeRecipient = _feeRecipient; + 558: feeRecipient = NewfeeRecipient; 559: }
constants
should be defined rather than using magic numbersEven assembly can benefit from using readable constants instead of hex/numeric literals
src/vault/Vault.sol: 34 35: uint256 constant SECONDS_PER_YEAR = 365.25 days; 619: uint256 public quitPeriod = 3 days;
Changing fee
makes a critical and important value change for users, therefore it is recommended to use timelock
1 result - 1 file src/vault/VaultController.sol: 750 */ 751: function setPerformanceFee(uint256 newFee) external onlyOwner { 752: // Dont take more than 20% performanceFee 753: if (newFee > 2e17) revert InvalidPerformanceFee(newFee); 754: 755: emit PerformanceFeeChanged(performanceFee, newFee); 756: 757: performanceFee = newFee; 758: }
delete
keyword instead of assigning a value of 0
Using the 'delete' keyword instead of assigning a '0' value is a detailed optimization that increases code readability and audit quality, and clearly indicates the intent.
Other hand, if use delete
instead 0
value assign , it will be gas saved
2 results - 2 files src/utils/MultiRewardStaking.sol: - 186: accruedRewards[user][_rewardTokens[i]] = 0; + 186: delete accruedRewards[user][_rewardTokens[i]]; src/vault/adapter/abstracts/AdapterBase.sol: - 577: underlyingBalance = 0; + 577: delete underlyingBalance;
constant/immutable
variables6 results - 3 files src/utils/MultiRewardStaking.sol: 438: uint256 internal INITIAL_CHAIN_ID; 439: bytes32 internal INITIAL_DOMAIN_SEPARATOR; src/vault/Vault.sol: 657: uint256 internal INITIAL_CHAIN_ID; 658: bytes32 internal INITIAL_DOMAIN_SEPARATOR; src/vault/adapter/abstracts/AdapterBase.sol: 625: uint256 internal INITIAL_CHAIN_ID; 626: bytes32 internal INITIAL_DOMAIN_SEPARATOR;
inherited state variables
Context: MultiRewardStaking.sol#L121-L134
Description:
In MultiRewardStaking.sol#L121-L134
, there is a local variable named owner
, but there is a state varible named owner
in the inherited OwnedUpgradeable.sol
with the same name. This use causes compilers to issue warnings, negatively affecting checking and code readability.
And some IDE and code check programs give this warning:
INHERITED ❗SHADOWED❗(null) StateVar OwnedUpgradeable.owner
src/utils/MultiRewardStaking.sol: 121: function _withdraw( 122: address caller, 123: address receiver, 124: address owner, 125: uint256 assets, 126: uint256 shares 127: ) internal override accrueRewards(caller, receiver) { 128: if (caller != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); 129: 130: _burn(owner, shares); 131: IERC20(asset()).safeTransfer(receiver, assets); 132: 133: emit Withdraw(caller, receiver, owner, assets, shares); 134: }
Recommendation: Avoid using variables with the same name, including inherited in the same contract, if used, it must be specified in the NatSpec comments.
src/vault/AdminProxy.sol: 18 /// @notice Execute arbitrary management functions. 19: function execute(address target, bytes calldata callData) 20: external 21: onlyOwner 22: returns (bool success, bytes memory returndata) 23: { 24: return target.call(callData); 25: } 26 }
Recommendation: Consider adopting a consistent approach to return values throughout the codebase by removing all named return variables, explicitly declaring them as local variables, and adding the necessary return statements where appropriate. This would improve both the explicitness and readability of the code, and it may also help reduce regressions during future code refactors.
2 result src/vault/Vault.sol: 525: function proposeFees(VaultFees calldata newFees) external onlyOwner { 526: if ( 527: newFees.deposit >= 1e18 || 528: newFees.withdrawal >= 1e18 || 529: newFees.management >= 1e18 || 530: newFees.performance >= 1e18 531: ) revert InvalidVaultFees(); 532: 533: proposedFees = newFees; 534: proposedFeeTime = block.timestamp; 535: 536: emit NewFeesProposed(newFees, block.timestamp); 537: } src/vault/Vault.sol: 578: function proposeAdapter(IERC4626 newAdapter) external onlyOwner { 579: if (newAdapter.asset() != address(asset)) 580: revert VaultAssetMismatchNewAdapterAsset(); 581: 582: proposedAdapter = newAdapter; 583: proposedAdapterTime = block.timestamp; 584: 585: emit NewAdapterProposed(newAdapter, block.timestamp); 586: }
Recommendation: Any event emitted by Solidity includes the timestamp and block number implicitly. Adding it the second time just increases the cost of the transaction
src/utils/MultiRewardEscrow.sol: 153 */ 154: function claimRewards(bytes32[] memory escrowIds) external { + require(escrowIds.length< max escrowIdsLengt, "max length"); 155: for (uint256 i = 0; i < escrowIds.length; i++) { 156: bytes32 escrowId = escrowIds[i]; 157: Escrow memory escrow = escrows[escrowId]; 158: 159: uint256 claimable = _getClaimableAmount(escrow); 160: if (claimable == 0) revert NotClaimable(escrowId); 161: 162: escrows[escrowId].balance -= claimable; 163: escrows[escrowId].lastUpdateTime = block.timestamp.safeCastTo32(); 164: 165: escrow.token.safeTransfer(escrow.account, claimable); 166: emit RewardsClaimed(escrow.token, escrow.account, claimable); 167: } 168: }
Recommendation: Function calls made in unbounded loop are error-prone with potential resource exhaustion as it can trap the contract due to gas limitations or failed transactions. Consider bounding the loop length if the array is expected to be growing and/or handling a huge list of elements to avoid unnecessary gas wastage and denial of service.
Use msg.sender
if the code does not implement EIP-2771 trusted forwarder support.
Reference: https://eips.ethereum.org/EIPS/eip-2771
4 results - 1 file src/vault/adapter/abstracts/AdapterBase.sol: 118 uint256 shares = _previewDeposit(assets); 119: _deposit(_msgSender(), receiver, assets, shares); 120 137 uint256 assets = _previewMint(shares); 138: _deposit(_msgSender(), receiver, assets, shares); 139 181 182: _withdraw(_msgSender(), receiver, owner, assets, shares); 183 200 uint256 assets = _previewRedeem(shares); 201: _withdraw(_msgSender(), receiver, owner, assets, shares);
#0 - c4-judge
2023-02-28T23:48:57Z
dmvt marked the issue as grade-a
🌟 Selected for report: c3phas
Also found by: 0xSmartContract, 0xackermann, 0xdaydream, Aymen0909, CodingNameKiki, Dewaxindo, Diana, IllIllI, Madalad, NoamYakov, Pheonix, Polaris_tow, ReyAdmirado, Rolezn, arialblack14, atharvasama, cryptostellar5, descharre, eyexploit, lukris02, saneryee
69.8247 USDC - $69.82
Number | Optimization Details | Context |
---|---|---|
[G-01] | Gas overflow during iteration (DoS) | 1 |
[G-02] | Duplicated require()/if() checks should be refactored to a modifier or function | 4 |
[G-03] | State variables only set in the constructor should be declared immutable | 10 |
[G-04] | Using delete instead of setting mapping/state variable 0 saves gas | 2 |
[G-05] | Using storage instead of memory for structs/arrays saves gas | 7 |
[G-06] | Add unchecked {} for subtractions where the operands cannot underflow because of a previous require or if statement | 3 |
[G-07] | Use assembly to write address storage values | 19 |
[G-08] | Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead | 4 |
[G-09] | Don't use _msgSender() if not supporting EIP-2771 | 4 |
[G-10] | internal functions only called once can be inlined to save gas | 4 |
[G-11] | Emitting storage values instead of the memory one | 1 |
[G-12] | Superfluous event fields | 2 |
[G-13] | Empty blocks should be removed or emit something | 7 |
[G-14] | Setting the constructor to payable (117 gas) | 9 |
[G-15] | x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables | 2 |
[G-16] | >= costs less gas than > | 4 |
[G-17] | Use nested if and, avoid multiple check combinations | 2 |
[G-18] | Sort Solidity operations using short-circuit mode | 5 |
[G-19] | Optimize names to save gas (22 gas) | All contracts |
[G-20] | External visibility can be used in public functions | 13 |
[G-21] | Use a more recent version of solidity | All contracts |
[G-22] | Cheaper to calculate domain separator every time | 3 |
[G-23] | Use hardcode address instead address(this) | 20 |
Total 23 issues
Each iteration of the cycle requires a gas flow. A moment may come when more gas is required than it is allocated to record one block. In this case, all iterations of the loop will fail.
1 result - 1 file:
src/utils/MultiRewardEscrow.sol: 153 */ 154: function claimRewards(bytes32[] memory escrowIds) external { + require(escrowIds.length< max escrowIdsLengt, "max length"); 155: for (uint256 i = 0; i < escrowIds.length; i++) { 156: bytes32 escrowId = escrowIds[i]; 157: Escrow memory escrow = escrows[escrowId]; 158: 159: uint256 claimable = _getClaimableAmount(escrow); 160: if (claimable == 0) revert NotClaimable(escrowId); 161: 162: escrows[escrowId].balance -= claimable; 163: escrows[escrowId].lastUpdateTime = block.timestamp.safeCastTo32(); 164: 165: escrow.token.safeTransfer(escrow.account, claimable); 166: emit RewardsClaimed(escrow.token, escrow.account, claimable); 167: } 168: }
Here are the data available in the covered contracts. The use of this situation in contracts that are not covered will also provide gas optimization.
4 results - 1 file:
src\vault\Vault.sol: 141: if (receiver == address(0)) revert InvalidReceiver(); 177: if (receiver == address(0)) revert InvalidReceiver(); 216: if (receiver == address(0)) revert InvalidReceiver(); 258: if (receiver == address(0)) revert InvalidReceiver();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L141
Recommendation:
You can consider adding a modifier like below.
modifer checkReceiver () { if (receiver == address(0)) revert InvalidReceiver(); _; }
Here are the data available in the covered contracts. The use of this situation in contracts that are not covered will also provide gas optimization.
Avoids a Gsset (20000 gas) in the constructor and replaces the first access in each transaction (Gcoldsload - 2100 gas) and each access thereafter (Gwarmacces - 100 gas) with a PUSH32 (3 gas).
10 result - 6 files:
src\utils\MultiRewardEscrow.sol: 30 constructor(address _owner, address _feeRecipient) Owned(_owner) { 31: feeRecipient = _feeRecipient;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardEscrow.sol#L31
src\vault\VaultController.sol: 53 constructor( 61: adminProxy = _adminProxy; 62: vaultRegistry = _vaultRegistry; 63: permissionRegistry = _permissionRegistry; 64: escrow = _escrow;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/VaultController.sol#L61-L64
src\utils\MultiRewardStaking.sol: 41 function initialize( 53: escrow = _escrow;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L53
src\vault\Vault.sol: 57 function initialize( 77: asset = asset_;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L77
src\vault\adapter\beefy\BeefyAdapter.sol: 46 function initialize( 73: beefyVault = IBeefyVault(_beefyVault); 74: beefyBooster = IBeefyBooster(_beefyBooster);
src\vault\adapter\yearn\YearnAdapter.sol: 34 function initialize( 45: yVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset));
delete
instead of setting mapping/state variable 0
saves gas2 results - 2 files:
src\utils\MultiRewardStaking.sol: 186: accruedRewards[user][_rewardTokens[i]] = 0;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L186
src\vault\adapter\abstracts\AdapterBase.sol: 577: underlyingBalance = 0;
Recommendation code:
src\vault\adapter\abstracts\AdapterBase.sol: - 577: underlyingBalance = 0; + 577: delete underlyingBalance;
storage
instead of memory
for structs/arrays
saves gasWhen fetching data from a storage location, assigning the data to a memory
variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory
keyword, declaring the variable with the storage
keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory
variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory
, or if the array/struct is being read from another memory
array/struct
7 results - 2 files:
src\utils\MultiRewardEscrow.sol: 157: Escrow memory escrow = escrows[escrowId];
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardEscrow.sol#L157
src\utils\MultiRewardStaking.sol: 176: EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; 254: RewardInfo memory rewards = rewardInfos[rewardToken]; 297: RewardInfo memory rewards = rewardInfos[rewardToken]; 328: RewardInfo memory rewards = rewardInfos[rewardToken]; 375: RewardInfo memory rewards = rewardInfos[rewardToken]; 414: RewardInfo memory rewards = rewardInfos[_rewardToken];
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L176
unchecked {}
for subtractions where the operands cannot underflow because of a previous require
or if
statementrequire(a <= b); x = b - a => require(a <= b); unchecked { x = b - a } if(a <= b); x = b - a => if(a <= b); unchecked { x = b - a }
This will stop the check for overflow and underflow so it will save gas.
3 results - 1 file:
src\utils\MultiRewardStaking.sol: 356: if (rewardsEndTimestamp > block.timestamp) 357 amount += uint256(rewardsPerSecond) * (rewardsEndTimestamp - block.timestamp); 392: if (rewards.rewardsEndTimestamp > block.timestamp) { 393 elapsed = block.timestamp - rewards.lastUpdatedTimestamp; 394: } else if (rewards.rewardsEndTimestamp > rewards.lastUpdatedTimestamp) { 395 elapsed = rewards.rewardsEndTimestamp - rewards.lastUpdatedTimestamp; 396 }
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L356-L357 https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L392-L396
src\vault\adapter\abstracts\AdapterBase.sol: 535: performanceFee_ > 0 && shareValue > highWaterMark_ 536 ? performanceFee_.mulDiv( 537 (shareValue - highWaterMark_) * totalSupply(), 538 1e36, 539 Math.Rounding.Down 540 ) 541 : 0;
src\vault\adapter\yearn\YearnAdapter.sol: 150: if (assets >= _depositLimit) return 0; 151 return _depositLimit - assets;
assembly
to write address storage values19 results - 5 files:
src\utils\MultiRewardEscrow.sol: 30 constructor(address _owner, address _feeRecipient) Owned(_owner) { 31: feeRecipient = _feeRecipient;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardEscrow.sol#L31
src\vault\DeploymentController.sol: 35 constructor( 41: cloneFactory = _cloneFactory; 42: cloneRegistry = _cloneRegistry; 43: templateRegistry = _templateRegistry;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/DeploymentController.sol#L41-L43
src\vault\VaultController.sol: 53 constructor( 61: adminProxy = _adminProxy; 62: vaultRegistry = _vaultRegistry; 63: permissionRegistry = _permissionRegistry; 64: escrow = _escrow; 836 function _setDeploymentController(IDeploymentController _deploymentController) internal { 842: deploymentController = _deploymentController;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/VaultController.sol#L61-L64
src\utils\MultiRewardStaking.sol: 41 function initialize( 53: escrow = _escrow;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L53
src\vault\Vault.sol: 57 function initialize( 77: asset = asset_; 78: adapter = adapter_; 88: fees = fees_; 91: feeRecipient = feeRecipient_; 525 function proposeFees(VaultFees calldata newFees) external onlyOwner { 533: proposedFees = newFees; 540 function changeFees() external { 545: fees = proposedFees; 553 function setFeeRecipient(address _feeRecipient) external onlyOwner { 558: feeRecipient = _feeRecipient; 578 function proposeAdapter(IERC4626 newAdapter) external onlyOwner { 582: proposedAdapter = newAdapter; 594 function changeAdapter() external takeFees { 608: adapter = proposedAdapter;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L77-L78
Recommendation Code:
src\vault\Vault.sol: 57 function initialize( - 77: asset = asset_; + assembly { + sstore(asset.slot, asset _) + }
When using elements that are smaller than 32 bytes, your contracts gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html
Use a larger size then downcast where needed.
4 results - 4 files:
src\utils\MultiRewardStaking.sol: //@audit uint8 _decimals 51: _decimals = IERC20Metadata(address(_stakingToken)).decimals();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L51
src\vault\Vault.sol: //@audit uint8 _decimals 82: _decimals = IERC20Metadata(address(asset_)).decimals();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L82
src\vault\adapter\abstracts\AdapterBase.sol: //@audit uint8 _decimals 77: _decimals = IERC20Metadata(asset).decimals();
src\utils\MultiRewardStaking.sol: //@audit uint160 rewardsPerSecond 359: return (block.timestamp + (amount / uint256(rewardsPerSecond))).safeCastTo32();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L359
Use msg.sender
if the code does not implement EIP-2771 trusted forwarder support.
Reference: https://eips.ethereum.org/EIPS/eip-2771
4 results - 1 file:
src\vault\adapter\abstracts\AdapterBase.sol: 119: _deposit(_msgSender(), receiver, assets, shares); 138: _deposit(_msgSender(), receiver, assets, shares); 182: _withdraw(_msgSender(), receiver, owner, assets, shares); 201: _withdraw(_msgSender(), receiver, owner, assets, shares);
internal
functions only called once can be inlined to save gasNot inlining costs 20 to 40 gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
4 results - 2 files:
src\utils\MultiRewardStaking.sol: 191 function _lockToken( 192 address user, 193 IERC20 rewardToken, 194 uint256 rewardAmount, 195 EscrowInfo memory escrowInfo 196: ) internal {
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L191-L202
src\vault\VaultController.sol: 141 function _registerCreatedVault( 142 address vault, 143 address staking, 144 VaultMetadata memory metadata 145: ) internal { 154: function _handleVaultStakingRewards(address vault, bytes memory rewardsData) internal { 692: function _verifyAdapterConfiguration(address adapter, bytes32 adapterId) internal view {
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/VaultController.sol#L141-L151
Reading the values published below from memory
instead of storage
saves gas.
1 result - 1 file:
src\vault\Vault.sol: 629: function setQuitPeriod(uint256 _quitPeriod) external onlyOwner { 630: if (_quitPeriod < 1 days || _quitPeriod > 7 days) 631: revert InvalidQuitPeriod(); 632: 633: quitPeriod = _quitPeriod; 634: 635: emit QuitPeriodSet(quitPeriod); 636: }
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L629-L636
src\vault\Vault.sol: 629: function setQuitPeriod(uint256 _quitPeriod) external onlyOwner { 630: if (_quitPeriod < 1 days || _quitPeriod > 7 days) 631: revert InvalidQuitPeriod(); 632: 633: quitPeriod = _quitPeriod; 634: - 635: emit QuitPeriodSet(quitPeriod); + 635: emit QuitPeriodSet(_quitPeriod); 636: }
block.timestamp
and block.number
are added to event information by default so adding them manually wastes gas.
2 result - 1 file: src\vault\Vault.sol: 512: event NewFeesProposed(VaultFees newFees, uint256 timestamp); 569: event NewAdapterProposed(IERC4626 newAdapter, uint256 timestamp);
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L512 https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L569
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should be abstract and the function signatures be added without any default implementation. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}}). Empty receive()/fallback() payable functions that are not used, can be removed to save deployment gas.
7 results - 4 files:
src\utils\EIP165.sol: 7: function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/EIP165.sol#L7
src\vault\Vault.sol: 473 function takeManagementAndPerformanceFees() 474 external 475 nonReentrant 476 takeFees 477: {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L477
src\vault\adapter\abstracts\AdapterBase.sol: 258: function _totalAssets() internal view virtual returns (uint256) {} 265: function _underlyingBalance() internal view virtual returns (uint256) {} 271 function convertToUnderlyingShares(uint256 assets, uint256 shares) 272 public 273 view 274 virtual 275 returns (uint256) 276: {}
src\vault\adapter\abstracts\WithRewards.sol: 13: function rewardTokens() external view virtual returns (address[] memory) {} 15: function claim() public virtual onlyStrategy {}
payable
(117 gas)You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0
and saves 13 gas
on deployment with no security risks.
9 results - 9 files:
src\utils\MultiRewardEscrow.sol: 30: constructor(address _owner, address _feeRecipient) Owned(_owner) {
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardEscrow.sol#L30
src\vault\AdminProxy.sol: 16: constructor(address _owner) Owned(_owner) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/AdminProxy.sol#L16
src\vault\CloneFactory.sol: 23: constructor(address _owner) Owned(_owner) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/CloneFactory.sol#L23
src\vault\CloneRegistry.sol: 22: constructor(address _owner) Owned(_owner) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/CloneRegistry.sol#L22
src\vault\DeploymentController.sol: 35: constructor(
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/DeploymentController.sol#L35
src\vault\PermissionRegistry.sol: 20: constructor(address _owner) Owned(_owner) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/PermissionRegistry.sol#L20
src\vault\TemplateRegistry.sol: 24: constructor(address _owner) Owned(_owner) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/TemplateRegistry.sol#L24
src\vault\VaultController.sol: 53: constructor(
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/VaultController.sol#L53
Recommendation:
Set the constructor to payable
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variables2 results - 2 files:
src\vault\adapter\abstracts\AdapterBase.sol: 158: underlyingBalance += _underlyingBalance() - underlyingBalance_; 225: underlyingBalance -= underlyingBalance_ - _underlyingBalance();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L158 https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L225
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variables.
>=
costs less gas than >
The compiler uses opcodes GT and ISZERO for solidity code that uses >, but only requires LT for >=, which saves 3 gas
4 results - 3 files:
src\utils\MultiRewardStaking.sol: 309: prevEndTime > block.timestamp ? prevEndTime : block.timestamp.safeCastTo32(),
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L309
src\vault\Vault.sol: 431 return 432: managementFee > 0 433 ? managementFee.mulDiv( 434 totalAssets() * (block.timestamp - feesUpdatedAt), 435 SECONDS_PER_YEAR, 436 Math.Rounding.Down 437 ) / 1e18 438 : 0; 452 return 453: performanceFee > 0 && shareValue > highWaterMark 454 ? performanceFee.mulDiv( 455 (shareValue - highWaterMark) * totalSupply(), 456 1e36, 457 Math.Rounding.Down 458 ) 459 : 0;
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L431-L438 https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L452-L459
src\vault\adapter\abstracts\AdapterBase.sol: 534 return 535: performanceFee_ > 0 && shareValue > highWaterMark_ 536 ? performanceFee_.mulDiv( 537 (shareValue - highWaterMark_) * totalSupply(), 538 1e36, 539 Math.Rounding.Down 540 ) 541 : 0;
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
2 results - 2 files:
src\vault\PermissionRegistry.sol: 43: if (newPermissions[i].endorsed && newPermissions[i].rejected) revert Mismatch();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/PermissionRegistry.sol#L43
src\vault\Vault.sol: 490: if (totalFee > 0 && currentAssets > 0) 491 _mint(feeRecipient, convertToShares(totalFee));
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L490
Recomendation Code:
src\vault\Vault.sol: - 490: if (totalFee > 0 && currentAssets > 0) + if (totalFee > 0) + if (currentAssets > 0) { 491 _mint(feeRecipient, convertToShares(totalFee)); + } + }
Short-circuiting is a solidity contract development model that uses OR/AND
logic to sequence different cost operations. It puts low gas cost operations in the front and high gas cost operations in the back, so that if the front is low If the cost operation is feasible, you can skip (short-circuit) the subsequent high-cost Ethereum virtual machine operation.
//f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)
5 results - 4 files:
src\utils\MultiRewardStaking.sol: 481: if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress);
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L481
src\vault\Vault.sol: 702: if (recoveredAddress == address(0) || recoveredAddress != owner)
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L702
src\vault\VaultController.sol: 669: if (msg.sender != metadata.creator || msg.sender != owner) revert NotSubmitterNorOwner(msg.sender); 837: if (address(_deploymentController) == address(0) || address(deploymentController) == address(_deploymentController))
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/VaultController.sol#L669
src\vault\adapter\abstracts\AdapterBase.sol: 670: if (recoveredAddress == address(0) || recoveredAddress != owner)
Contracts most called functions could simply save gas by function ordering via Method ID
. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas
are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
Context: All Contracts
Recommendation:
Find a lower method ID
name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas
For example, the function IDs in the VaultController.sol
contract will be the most used; A lower method ID may be given.
Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92
VaultController.sol function names can be named and sorted according to METHOD ID
Sighash | Function Signature ======================== 68432477 => setPermissions(address[],Permission[]) 69e3b3dc => deployVault(VaultInitParams,DeploymentArgs,DeploymentArgs,address,bytes,VaultMetadata,uint256) 9cfc553d => _deployVault(VaultInitParams,IDeploymentController) 6cfc1202 => _registerCreatedVault(address,address,VaultMetadata) 459f12e4 => _handleVaultStakingRewards(address,bytes) 71b7ae93 => _handleInitialDeposit(uint256,IERC20,IERC4626) fc263a50 => deployAdapter(IERC20,DeploymentArgs,DeploymentArgs,uint256) 96c2b2bd => _deployAdapter(IERC20,DeploymentArgs,DeploymentArgs,IDeploymentController) f1959d73 => __deployAdapter(DeploymentArgs,bytes,IDeploymentController) 8515c57f => _encodeAdapterData(DeploymentArgs,bytes) 5b4c2524 => _deployStrategy(DeploymentArgs,IDeploymentController) e4f49a99 => deployStaking(IERC20) c461a414 => _deployStaking(IERC20,IDeploymentController) 40a2c775 => proposeVaultAdapters(address[],IERC4626[]) 5c456e16 => changeVaultAdapters(address[]) 0027fdc7 => proposeVaultFees(address[],VaultFees[]) 3c27cc18 => changeVaultFees(address[]) 6cd77c1b => _registerVault(address,VaultMetadata) ccc0f886 => addStakingRewardsTokens(address[],bytes[]) 87b31809 => changeStakingRewardsSpeeds(address[],IERC20[],uint160[]) 26699bcc => fundStakingRewards(address[],IERC20[],uint256[]) ecd176d1 => setEscrowTokenFees(IERC20[],uint256[]) 5dcc700f => addTemplateCategories(bytes32[]) 7bd0703d => toggleTemplateEndorsements(bytes32[],bytes32[]) a52f0ead => pauseAdapters(address[]) 8ba684e0 => pauseVaults(address[]) 6d956763 => unpauseAdapters(address[]) 2c3f064f => unpauseVaults(address[]) 0bbc1ffc => _verifyCreatorOrOwner(address) c7db9835 => _verifyCreator(address) 5cbea48b => _verifyToken(address) 33785a9f => _verifyAdapterConfiguration(address,bytes32) 23f4c026 => _verifyEqualArrayLength(uint256,uint256) 0a95d1ff => nominateNewAdminProxyOwner(address) e45e1b94 => acceptAdminProxyOwnership() 70897b23 => setPerformanceFee(uint256) d7fe6a85 => setAdapterPerformanceFees(address[]) d643ad32 => setHarvestCooldown(uint256) 23e45a62 => setAdapterHarvestCooldowns(address[]) eab63b2a => setDeploymentController(IDeploymentController) 46ef81f4 => _setDeploymentController(IDeploymentController) ebf0f0db => setActiveTemplateId(bytes32,bytes32)
The appearance of the following functions can be changed from public to external.
13 results - 5 files:
src\utils\EIP165.sol: 7: function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {}
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/EIP165.sol#L7
src\vault\Vault.sol: 124: function deposit(uint256 assets) public returns (uint256) { 200: function withdraw(uint256 assets) public returns (uint256) { 211 function withdraw( 212 uint256 assets, 213 address receiver, 214 address owner 215: ) public nonReentrant syncFeeCheckpoint returns (uint256 shares) { 253 function redeem( 254 uint256 shares, 255 address receiver, 256 address owner 257: ) public nonReentrant returns (uint256 assets) { 340: function previewMint(uint256 shares) public view returns (uint256 assets) { 664 function permit( 665 address owner, 666 address spender, 667 uint256 value, 668 uint256 deadline, 669 uint8 v, 670 bytes32 r, 671 bytes32 s 672: ) public virtual {
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L124
src\vault\adapter\abstracts\AdapterBase.sol: 173 function withdraw( 174 uint256 assets, 175 address receiver, 176 address owner 177: ) public virtual override returns (uint256) { 193 function redeem( 194 uint256 shares, 195 address receiver, 196 address owner 197: ) public virtual override returns (uint256) { 549: function setPerformanceFee(uint256 newFee) public onlyOwner {
src\vault\adapter\abstracts\WithRewards.sol: 15: function claim() public virtual onlyStrategy {} 21: function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
src\vault\adapter\yearn\YearnAdapter.sol: 144: function maxDeposit(address) public view override returns (uint256) {
In 0.8.15 the conditions necessary for inlining are relaxed. Benchmarks show that the change significantly decreases the bytecode size (which impacts the deployment cost) while the effect on the runtime gas usage is smaller.
In 0.8.17 prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call; Simplify the starting offset of zero-length operations to zero. More efficient overflow checks for multiplication.
All contracts in scope (35 files) are written in pragma solidity ^0.8.15;
and I recommend using the newer battle-tested version of Solidity 0.8.17
.
Context: All contracts
Since INITIAL_CHAIN_ID and INITIAL_DOMAIN_SEPARATOR are no longer immutable, but are state variables, you end up looking up their value every time, which incurs a very large gas penalty.
Use OpenZeppelin's version which is optimized for this case. https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol
3 results - 3 files:
src\utils\MultiRewardStaking.sol: 488: return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L488
src\vault\Vault.sol: 710: return 711: block.chainid == INITIAL_CHAIN_ID 712: ? INITIAL_DOMAIN_SEPARATOR 713: : computeDomainSeparator(); 714: }
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L710-L714
src\vault\adapter\abstracts\AdapterBase.sol: 678: return 679: block.chainid == INITIAL_CHAIN_ID 680: ? INITIAL_DOMAIN_SEPARATOR 681: : computeDomainSeparator(); 682: }
address(this)
Instead of address(this)
, it is more gas-efficient to pre-calculate and use the address with a hardcode. Foundry's script.sol
and solmate````LibRlp.sol`` contracts can do this.
Reference: https://book.getfoundry.sh/reference/forge-std/compute-create-address https://twitter.com/transmissions11/status/1518507047943245824
20 results - 8 files
src\utils\MultiRewardEscrow.sol: 100: token.safeTransferFrom(msg.sender, address(this), amount);
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardEscrow.sol#L100
src\utils\MultiRewardStaking.sol: 113: IERC20(asset()).safeTransferFrom(caller, address(this), assets); 259: rewardToken.safeTransferFrom(msg.sender, address(this), amount); 305: uint256 remainder = rewardToken.balanceOf(address(this)); 334: rewardToken.safeTransferFrom(msg.sender, address(this), amount);
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/utils/MultiRewardStaking.sol#L113
src\vault\Vault.sol: 153: asset.safeTransferFrom(msg.sender, address(this), assets); 154 155: adapter.deposit(assets, address(this)); 193: asset.safeTransferFrom(msg.sender, address(this), assets); 194 195: adapter.deposit(assets, address(this)); 237: adapter.withdraw(assets, receiver, address(this)); 275: adapter.withdraw(assets, receiver, address(this)); 286: return adapter.convertToAssets(adapter.balanceOf(address(this))); 612: adapter.deposit(asset.balanceOf(address(this)), address(this));
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/Vault.sol#L153
src\vault\VaultController.sol: 170: asset.safeTransferFrom(msg.sender, address(this), initialDeposit); 526: rewardTokens[i].transferFrom(msg.sender, address(this), amounts[i]);
https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/VaultController.sol#L170
src\vault\adapter\abstracts\AdapterBase.sol: 153: IERC20(asset()).safeTransferFrom(caller, address(this), assets);
src\vault\adapter\abstracts\OnlyStrategy.sol: 11: if (msg.sender != address(this)) revert NotStrategy(msg.sender);
src\vault\adapter\beefy\BeefyAdapter.sol: 118: return beefyBalanceCheck.balanceOf(address(this)); 199: beefyBooster.stake(beefyVault.balanceOf(address(this)));
src\vault\adapter\yearn\YearnAdapter.sol: 85: return yVault.balanceOf(address(this));
#0 - c4-judge
2023-02-28T14:53:27Z
dmvt marked the issue as grade-b