Platform: Code4rena
Start Date: 07/07/2022
Pot Size: $75,000 USDC
Total HM: 32
Participants: 141
Period: 7 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 144
League: ETH
Rank: 46/141
Findings: 4
Award: $213.57
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: infosec_us_team
Also found by: 0x29A, 0xsanson, BowTiedWardens, Lambda, MEP, PwnedNoMore, Treasure-Seeker, TrungOre, berndartmueller, panprog, shenwilly, smiling_heretic, xiaoming90, zzzitron
Issue/consequence: users can withdraw their contributions from migrations that are not committed yet causing a malfunction of the protocol. E.g. someone can add proposals then it can withdraw them using this method, as the totalEth and totalFractions of a proposal are not decreased during this method, the users can manipulate the price on commit. This is possible due to the fact that there is no correlation between _vault parameter and _proposalId, so someone can call this function with a vault that is not migrated yet and different proposalIds.
Affected Code
File: Migration.sol 289: /// @notice Retrieves ether and fractions deposited from an unsuccessful migration 290: /// @param _vault Address of the vault 291: /// @param _proposalId ID of the failed proposal 292: function withdrawContribution(address _vault, uint256 _proposalId) // @audit-info [HIGH] 293: external 294: { 295: // Reverts if address is not a registered vault 296: (address token, uint256 id) = IVaultRegistry(registry).vaultToToken( 297: _vault 298: ); 299: if (id == 0) revert NotVault(_vault); 300: // Reverts if caller has no fractional balance to withdraw 301: (, , State current, , , ) = IBuyout(buyout).buyoutInfo(_vault); 302: if ( 303: current != State.INACTIVE || 304: migrationInfo[_vault][_proposalId].newVault != address(0) 305: ) revert NoContributionToWithdraw(); 306: 307: // Temporarily store user's fractions for the transfer 308: uint256 userFractions = userProposalFractions[_proposalId][msg.sender]; 309: // Updates fractional balance of caller 310: userProposalFractions[_proposalId][msg.sender] = 0; 311: // Withdraws fractional tokens from contract back to caller 312: IFERC1155(token).safeTransferFrom( 313: address(this), 314: msg.sender, 315: id, 316: userFractions, 317: "" 318: ); 319: 320: // Temporarily store user's eth for the transfer 321: uint256 userEth = userProposalEth[_proposalId][msg.sender]; 322: // Updates ether balance of caller 323: userProposalEth[_proposalId][msg.sender] = 0; 324: // Withdraws ether from contract back to caller 325: payable(msg.sender).transfer(userEth); 326: }
Recommendation: subtract totalFractions and totalEth when withdrawing (like you do on the leave function) or make sure the migrateInfo and proposalId are correlated. Currently there is no check between proposalId and vault, except newVault which is address(0) most of the time.
File: Migration.sol 302: if ( 303: current != State.INACTIVE || 304: migrationInfo[_vault][_proposalId].newVault != address(0) 305: ) revert NoContributionToWithdraw();
#0 - 0x0aa0
2022-07-20T19:36:49Z
Duplicate of #27
๐ Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, Amithuddar, Avci, BowTiedWardens, Kthere, Limbooo, MEP, Ruhum, StyxRave, TomJ, Treasure-Seeker, TrungOre, Tutturu, Waze, bardamu, c3phas, cccz, codexploder, cryptphi, hake, horsefacts, hyh, oyc_109, pashov, peritoflores, scaraven, simon135, slywaters, sseefried, tofunmi, xiaoming90
1.3977 USDC - $1.40
https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L172 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L325
This is a classic Code4rena issue:
The use of the deprecated transfer()
function for an address will inevitably make the transaction fail when:
Additionally, using higher than 2300 gas might be mandatory for some multisig wallets.
modules/Migration.sol:172: payable(msg.sender).transfer(ethAmount); modules/Migration.sol:325: payable(msg.sender).transfer(userEth);
Use call()
instead of transfer()
, but be sure to implement CEI patterns in CEther and add a global state lock on the comptroller as per Rari.
THIS HAS REKT COMPOUND FORKS BEFORE!!!
relevant links: https://twitter.com/hacxyk/status/1520715516490379264?s=21&t=fnhDkcC3KpE_kJE8eLiE2A https://twitter.com/hacxyk/status/1520715536325218304?s=21&t=fnhDkcC3KpE_kJE8eLiE2A https://twitter.com/hacxyk/status/1520370441705037824?s=21&t=fnhDkcC3KpE_kJE8eLiE2A https://twitter.com/Hacxyk/status/1521949933380595712
#0 - stevennevins
2022-07-19T21:45:47Z
Duplicate of #325
#1 - HardlyDifficult
2022-07-28T15:45:54Z
Duping to #504
๐ Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 242, 8olidity, Amithuddar, Aymen0909, Bnke0x0, BowTiedWardens, David_, Deivitto, ElKu, Funen, Hawkeye, IllIllI, JC, Kaiziron, Keen_Sheen, Kthere, Kulk0, Kumpa, Lambda, MEP, ReyAdmirado, Rohan16, Ruhum, Sm4rty, TomJ, Tomio, Treasure-Seeker, TrungOre, Tutturu, Viksaa39, Waze, _Adam, __141345__, ak1, apostle0x01, asutorufos, async, ayeslick, aysha, bbrho, benbaessler, berndartmueller, c3phas, cccz, chatch, cloudjunky, codexploder, cryptphi, delfin454000, dipp, durianSausage, dy, exd0tpy, fatherOfBlocks, hake, hansfriese, horsefacts, hubble, joestakey, jonatascm, kebabsec, kenzo, kyteg, mektigboy, neumo, oyc_109, pashov, pedr02b2, peritoflores, rajatbeladiya, rbserver, robee, rokinot, s3cunda, sach1r0, sahar, sashik_eth, scaraven, shenwilly, simon135, sorrynotsorry, sseefried, svskaushik, unforgiven, z3s, zzzitron
106.3285 USDC - $106.33
Overview
Risk Rating | Number of issues |
---|---|
Low Risk | 8 |
Non-Critical Risk | 5 |
Table of Contents
nonReentrant
modifier should be appliedtransfer()
/transferFrom()
with IERC20
receive()
function_safeMint()
should be used rather than _mint()
wherever possiblenonReentrant
modifier should be appliedexternal
/public
functions without access-control that modify the state after an external call should be nonReentrant
as a best practice to reduce the attack surface.
Keep in mind that IERC1155(token).safeTransferFrom()
has a callback on onERC1155Received
that opens way for a potential re-entrancy:
File: ERC1155.sol 55: function safeTransferFrom( ... 69: require( 70: to.code.length == 0 71: ? to != address(0) 72: : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, amount, data) == 73: ERC1155TokenReceiver.onERC1155Received.selector, 74: "UNSAFE_RECIPIENT" 75: ); 76: }
Here, the Check Effects Interactions Pattern cannot be applied (the values used are determined after the external call). The nonReentrant
modifier should then be used here (see @audit
tag):
File: Buyout.sol 57: function start(address _vault) external payable { //@audit should be nonReentrant ... 75: // Transfers fractional tokens into the buyout pool 76: IERC1155(token).safeTransferFrom( //@audit : external call (Interaction) 77: msg.sender, 78: address(this), 79: id, 80: depositAmount, 81: "" 82: ); ... 91: buyoutInfo[_vault] = Auction( //@audit : modifying the state after the external call (Effect, CEIP not respected). Consider marking this function as nonReentrant 92: block.timestamp, 93: msg.sender, 94: State.LIVE, 95: fractionPrice, 96: msg.value, 97: totalSupply 98: );
Consider moving the state-changes before the external calls here:
File: Buyout.sol 112: function sellFractions(address _vault, uint256 _amount) external { ... 128: // Transfers fractional tokens to pool from caller 129: IERC1155(token).safeTransferFrom( //@audit CEIP not respected + 130: msg.sender, 131: address(this), 132: id, 133: _amount, 134: "" 135: ); 136: 137: // Updates ether balance of pool 138: uint256 ethAmount = fractionPrice * _amount; 139: buyoutInfo[_vault].ethBalance -= ethAmount; //@audit CEIP not respected 140: // Transfers ether amount to caller 141: _sendEthOrWeth(msg.sender, ethAmount); 142: // Emits event for selling fractions into pool 143: emit SellFractions(msg.sender, _amount); 144: }
File: Buyout.sol 149: function buyFractions(address _vault, uint256 _amount) external payable { ... 167: // Transfers fractional tokens to caller 168: IERC1155(token).safeTransferFrom( 169: address(this), 170: msg.sender, 171: id, 172: _amount, 173: "" 174: ); 175: // Updates ether balance of pool 176: buyoutInfo[_vault].ethBalance += msg.value; 177: // Emits event for buying fractions from pool 178: emit BuyFractions(msg.sender, _amount);
File: Buyout.sol 184: function end(address _vault, bytes32[] calldata _burnProof) external { ... 228: IERC1155(token).safeTransferFrom(//@audit CEIP not respected 229: address(this), 230: proposer, 231: id, 232: tokenBalance, 233: "" 234: ); 235: _sendEthOrWeth(proposer, ethBalance);//@audit CEIP not respected
File: Migration.sol 105: function join( .... 126: IFERC1155(token).safeTransferFrom( //@audit CEIP not respected 127: msg.sender, 128: address(this), 129: id, 130: _amount, 131: "" 132: ); 133: // Updates fraction balances of the proposal and caller 134: proposal.totalFractions += _amount; //@audit CEIP not respected 135: userProposalFractions[_proposalId][msg.sender] += _amount; //@audit CEIP not respected
File: Migration.sol 292: function withdrawContribution(address _vault, uint256 _proposalId) ... 312: IFERC1155(token).safeTransferFrom( //@audit CEIP not respected 313: address(this), 314: msg.sender, 315: id, 316: userFractions, 317: "" 318: ); 319: 320: // Temporarily store user's eth for the transfer 321: uint256 userEth = userProposalEth[_proposalId][msg.sender]; 322: // Udpates ether balance of caller 323: userProposalEth[_proposalId][msg.sender] = 0; //@audit CEIP not respected 324: // Withdraws ether from contract back to caller 325: payable(msg.sender).transfer(userEth);
Consider adding an address(0)
check for immutable variables:
File: SupplyReference.sol 12: address immutable registry; 13: 14: /// @notice Initializes address of registry contract 15: constructor(address _registry) { 16: registry = _registry; 17: }
File: Supply.sol 12: /// @notice Address of VaultRegistry contract 13: address immutable registry; 14: 15: /// @notice Initializes registry contract 16: constructor(address _registry) { 17: registry = _registry; 18: }
File: Metadata.sol 11: address immutable token; ... 15: /// @notice Initializes token contract 16: constructor(address _token) { 17: token = _token; 18: }
File: VaultRegistry.sol 17: address public immutable factory; 18: /// @notice Address of FERC1155 token contract 19: address public immutable fNFT; 20: /// @notice Address of Implementation for FERC1155 token contract 21: address public immutable fNFTImplementation; ... 27: /// @notice Initializes factory, implementation, and token contracts 28: constructor() { 29: factory = address(new VaultFactory()); 30: fNFTImplementation = address(new FERC1155()); 31: fNFT = fNFTImplementation.clone( 32: abi.encodePacked(msg.sender, address(this)) 33: ); 34: }
transfer()
/transferFrom()
with IERC20
Some tokens do not implement the ERC20 standard properly but are still accepted by most code that accepts ERC20 tokens. For example Tether (USDT)'s transfer()
and transferFrom()
functions do not return booleans as the specification requires, and instead have no return value. When these sorts of tokens are cast to IERC20
, their function signatures do not match and therefore the calls made, revert. Use OpenZeppelinโs SafeERC20
's safeTransfer()
/safeTransferFrom()
instead or check the return value.
Bad:
IERC20(token).transferFrom(msg.sender, address(this), amount);
Good (using OpenZeppelin's SafeERC20
):
import {SafeERC20} from "openzeppelin/token/utils/SafeERC20.sol"; // ... IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
Good (using require
):
bool success = IERC20(token).transferFrom(msg.sender, address(this), amount); require(success, "ERC20 transfer failed");
Consider using safeTransfer
/safeTransferFrom
or wrap in a require
statement here:
File: TransferReference.sol 17: function ERC20Transfer( 18: address _token, 19: address _to, 20: uint256 _value 21: ) external { 22: IERC20(_token).transfer(_to, _value); 23: }
File: BaseVault.sol 58: function batchDepositERC20( 59: address _from, 60: address _to, 61: address[] calldata _tokens, 62: uint256[] calldata _amounts 63: ) external { 64: for (uint256 i = 0; i < _tokens.length; ) { 65: IERC20(_tokens[i]).transferFrom(_from, _to, _amounts[i]); 66: unchecked { 67: ++i; 68: } 69: } 70: }
receive()
functionIf the intention is for the Ether to be used, the function should call another function, otherwise it should revert
modules/Buyout.sol:53: receive() external payable {} modules/Migration.sol:63: receive() external payable {} Vault.sol:32: receive() external payable {}
Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.
Consider adding a check to prevent accidentally burning tokens here:
File: TransferReference.sol 17: function ERC20Transfer( 18: address _token, 19: address _to, 20: uint256 _value 21: ) external { 22: IERC20(_token).transfer(_to, _value); 23: }
File: BaseVault.sol 58: function batchDepositERC20( 59: address _from, 60: address _to, 61: address[] calldata _tokens, 62: uint256[] calldata _amounts 63: ) external { 64: for (uint256 i = 0; i < _tokens.length; ) { 65: IERC20(_tokens[i]).transferFrom(_from, _to, _amounts[i]); 66: unchecked { 67: ++i; 68: } 69: } 70: }
_safeMint()
should be used rather than _mint()
wherever possible_mint()
is discouraged in favor of _safeMint()
which ensures that the recipient is either an EOA or implements ERC1155TokenReceiver
's onERC1155Received
.
File: FERC1155.sol 79: function mint( 80: address _to, 81: uint256 _id, 82: uint256 _amount, 83: bytes memory _data 84: ) external onlyRegistry { 85: _mint(_to, _id, _amount, _data); //@audit use safeMint 86: totalSupply[_id] += _amount; 87: }
Be careful however to respect the CEI pattern or add a re-entrancy guard as _safeMint
adds a callback-check and a malicious onERC1155Received
could be exploited if not careful.
Here, totalSupply[_id] += _amount;
will need to be moved before the new _safeMint
.
Reading material:
It's possible that the onlyOwner
role mistakenly transfers ownership to a wrong address, resulting in a loss of the onlyOwner
role.
Consider overriding the default transferOwnership()
function to first nominate an address as the pendingOwner
and implementing an acceptOwnership()
function which is called by the pendingOwner
to confirm the transfer.
File: Vault.sol 93: function transferOwnership(address _newOwner) external { 94: if (owner != msg.sender) revert NotOwner(owner, msg.sender); 95: owner = _newOwner; 96: emit TransferOwnership(msg.sender, _newOwner); 97: }
src/modules/protoforms/BaseVault.sol: 48: emit ActiveModules(vault, _modules); 49 50 _mintFractions(vault, msg.sender, _fractionSupply, _mintProof); 51 }
interfaces/IMigration.sol:23: // Address for the new vault to migrate to (if buyout is succesful) interfaces/IMigration.sol:25: // Boolean status to check if the propoal is active interfaces/IMigration.sol:29: // New fraction supply for a given vault that has succesfully migrated modules/Migration.sol:322: // Udpates ether balance of caller
Consider resolving the TODOs before deploying.
utils/MerkleBase.sol:24: // TODO: This can be aesthetically simplified with a switch. Not sure it will
string.concat()
or bytes.concat()
Solidity version 0.8.4 introduces bytes.concat()
(vs abi.encodePacked(<bytes>,<bytes>)
)
Solidity version 0.8.12 introduces string.concat()
(vs abi.encodePacked(<str>,<str>)
)
FERC1155.sol:2:pragma solidity 0.8.13; FERC1155.sol:395: abi.encodePacked("\x19\x01", _domainSeparator, _structHash) VaultFactory.sol:2:pragma solidity 0.8.13; VaultFactory.sol:40: .cloneCreationCode(abi.encodePacked()); VaultFactory.sol:47: abi.encodePacked(bytes1(0xff), address(this), salt, creationHash) VaultFactory.sol:68: bytes memory data = abi.encodePacked(); VaultRegistry.sol:2:pragma solidity 0.8.13; VaultRegistry.sol:32: abi.encodePacked(msg.sender, address(this)) VaultRegistry.sol:154: abi.encodePacked(_controller, address(this))
public
functions not called by the contract should be declared external
instead- MerkleBase.verifyProof(bytes32,bytes32[],bytes32) (src/utils/MerkleBase.sol#43-56) - Metadata.uri(uint256) (src/utils/Metadata.sol#39-41) - SelfPermit.selfPermit(address,uint256,bool,uint256,uint8,bytes32,bytes32) (src/utils/SelfPermit.sol#18-37) - SelfPermit.selfPermitAll(address,bool,uint256,uint8,bytes32,bytes32) (src/utils/SelfPermit.sol#46-63)
๐ Selected for report: joestakey
Also found by: 0x1f8b, 0x29A, 0xA5DF, 0xKitsune, 0xNazgul, 0xNineDec, 0xalpharush, 0xkatana, 0xsanson, 0xsolstars, 8olidity, Avci, Bnke0x0, BowTiedWardens, Chom, Deivitto, ElKu, Fitraldys, Funen, IllIllI, JC, Kaiziron, Lambda, Limbooo, MEP, NoamYakov, PwnedNoMore, RedOneN, ReyAdmirado, Rohan16, Ruhum, Saintcode_, Sm4rty, TomJ, Tomio, TrungOre, Tutturu, Waze, _Adam, __141345__, ajtra, apostle0x01, asutorufos, benbaessler, brgltd, c3phas, codexploder, cryptphi, delfin454000, dharma09, djxploit, durianSausage, fatherOfBlocks, giovannidisiena, gogo, horsefacts, hrishibhat, hyh, ignacio, jocxyen, jonatascm, karanctf, kebabsec, kyteg, m_Rassska, mektigboy, oyc_109, pedr02b2, rbserver, robee, rokinot, sach1r0, sashik_eth, simon135, slywaters
37.5523 USDC - $37.55
Overview
Risk Rating | Number of issues |
---|---|
Gas Issues | 9 |
Table of Contents:
immutable
<array>.length
should not be looked up in every loop of a for-loop
++i
costs less gas compared to i++
or i += 1
(same for --i
vs i--
or i -= 1
)payable
immutable
Variables only set in the constructor and never edited afterwards should be marked as immutable, as it would avoid the expensive storage-writing operation in the constructor (around 20 000 gas per variable) and replace the expensive storage-reading operations (around 2100 gas per reading) to a less expensive value reading (3 gas)
modules/protoforms/BaseVault.sol:19: address public registry; modules/Buyout.sol:29: address public registry; modules/Buyout.sol:31: address public supply; modules/Buyout.sol:33: address public transfer; modules/Migration.sol:37: address payable public buyout; modules/Migration.sol:39: address public registry; modules/Minter.sol:14: address public supply; VaultFactory.sol:15: address public implementation;
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Revert strings > 32 bytes:
utils/MerkleBase.sol:62: require(_data.length > 1, "wont generate root for single leaf"); utils/MerkleBase.sol:78: require(_data.length > 1, "wont generate proof for single leaf");
Consider shortening the revert strings to fit in 32 bytes.
While the DIV
/ MUL
opcode uses 5 gas, the SHR
/ SHL
opcode only uses 3 gas. Furthermore, beware that Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting. Eventually, overflow checks are never performed for shift operations as they are done for arithmetic operations. Instead, the result is always truncated, so the calculation can be unchecked in Solidity version 0.8+
>> 1
instead of / 2
>> 2
instead of / 4
<< 3
instead of * 8
Affected code (saves around 2 gas + 20 for unchecked per instance):
utils/MerkleBase.sol:100: _node = _node / 2; utils/MerkleBase.sol:136: result = new bytes32[](length / 2 + 1); utils/MerkleBase.sol:142: result = new bytes32[](length / 2);
<array>.length
should not be looked up in every loop of a for-loop
Reading array length at each iteration of the loop consumes more gas than necessary.
In the best case scenario (length read on a memory variable), caching the array length in the stack saves around 3 gas per iteration. In the worst case scenario (external calls at each iteration), the amount of gas wasted can be massive.
Here, consider storing the array's length in a variable before the for-loop, and use this new variable instead:
modules/protoforms/BaseVault.sol:64: for (uint256 i = 0; i < _tokens.length; ) { modules/protoforms/BaseVault.sol:83: for (uint256 i = 0; i < _tokens.length; ) { modules/protoforms/BaseVault.sol:107: for (uint256 i = 0; i < _tokens.length; ++i) { modules/protoforms/BaseVault.sol:130: for (uint256 i; i < _modules.length; ++i) { modules/protoforms/BaseVault.sol:132: for (uint256 j; j < leaves.length; ++j) { modules/Buyout.sol:454: for (uint256 i; i < permissions.length; ) { utils/MerkleBase.sol:51: for (uint256 i = 0; i < _proof.length; ++i) { utils/MerkleBase.sol:110: for (uint256 i; i < result.length; ++i) {
++i
costs less gas compared to i++
or i += 1
(same for --i
vs i--
or i -= 1
)Pre-increments and pre-decrements are cheaper.
For a uint256 i
variable, the following is true with the Optimizer enabled at 10k:
Increment:
i += 1
is the most expensive formi++
costs 6 gas less than i += 1
++i
costs 5 gas less than i++
(11 gas less than i += 1
)Decrement:
i -= 1
is the most expensive formi--
costs 11 gas less than i -= 1
--i
costs 5 gas less than i--
(16 gas less than i -= 1
)Note that post-increments (or post-decrements) return the old value before incrementing or decrementing, hence the name post-increment:
uint i = 1; uint j = 2; require(j == i++, "This will be false as i is incremented after the comparison");
However, pre-increments (or pre-decrements) return the new value:
uint i = 1; uint j = 2; require(j == ++i, "This will be true as i is incremented before the comparison");
In the pre-increment case, the compiler has to create a temporary variable (when used) for returning 1
instead of 2
.
Affected code:
utils/MerkleBase.sol:188: ceil++; Vault.sol:78: for (uint256 i = 0; i < length; i++) { Vault.sol:104: for (uint256 i = 0; i < length; i++) {
Consider using pre-increments and pre-decrements where they are relevant (meaning: not where post-increments/decrements logic are relevant).
In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
Consider wrapping with an unchecked
block here (around 25 gas saved per instance):
modules/protoforms/BaseVault.sol:107: for (uint256 i = 0; i < _tokens.length; ++i) { modules/protoforms/BaseVault.sol:130: for (uint256 i; i < _modules.length; ++i) { modules/protoforms/BaseVault.sol:132: for (uint256 j; j < leaves.length; ++j) { modules/Migration.sol:496: for (uint256 i; i < modulesLength; ++i) { modules/Migration.sol:504: for (uint256 i; i < modulesLength; ++i) { modules/Migration.sol:507: for (uint256 j; j < leavesLength; ++j) { utils/MerkleBase.sol:51: for (uint256 i = 0; i < _proof.length; ++i) { utils/MerkleBase.sol:110: for (uint256 i; i < result.length; ++i) { Vault.sol:78: for (uint256 i = 0; i < length; i++) { Vault.sol:104: for (uint256 i = 0; i < length; i++) {
The change would be:
- for (uint256 i; i < numIterations; i++) { + for (uint256 i; i < numIterations;) { // ... + unchecked { ++i; } }
The same can be applied with decrements (which should use break
when i == 0
).
The risk of overflow is non-existant for uint256
here.
If the optimizer is enabled, this finding isn't true anymore
If a variable is not set/initialized, it is assumed to have the default value (0
for uint
, false
for bool
, address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas (around 3 gas per instance).
As an example: for (uint256 i = 0; i < numIterations; ++i) {
should be replaced with for (uint256 i; i < numIterations; ++i) {
Affected code:
modules/protoforms/BaseVault.sol:64: for (uint256 i = 0; i < _tokens.length; ) { modules/protoforms/BaseVault.sol:83: for (uint256 i = 0; i < _tokens.length; ) { modules/protoforms/BaseVault.sol:107: for (uint256 i = 0; i < _tokens.length; ++i) { utils/MerkleBase.sol:51: for (uint256 i = 0; i < _proof.length; ++i) { Vault.sol:78: for (uint256 i = 0; i < length; i++) { Vault.sol:104: for (uint256 i = 0; i < length; i++) {
Consider removing explicit initializations for default values.
Solidity 0.8.4 introduced custom errors. They are more gas efficient than revert strings, when it comes to deploy cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for gas savings.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g.,
revert("Insufficient funds.");
), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error
statement, which can be used inside and outside of contracts (including interfaces and libraries).
Consider replacing all revert strings with custom errors in the solution.
utils/MerkleBase.sol:62: require(_data.length > 1, "wont generate root for single leaf"); utils/MerkleBase.sol:78: require(_data.length > 1, "wont generate proof for single leaf"); FERC1155.sol:263: require( FERC1155.sol:275: require( FERC1155.sol:297: require(metadata[_id] != address(0), "NO METADATA");
payable
If a function modifier such as onlyController
is used, the function will revert if a normal user tries to pay the function. Marking the function as payable
will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
FERC1155.sol:198: function setContractURI(string calldata _uri) external onlyController {