Platform: Code4rena
Start Date: 04/05/2022
Pot Size: $50,000 DAI
Total HM: 24
Participants: 71
Period: 5 days
Judge: Justin Goro
Total Solo HM: 14
Id: 119
League: ETH
Rank: 24/71
Findings: 2
Award: $281.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: horsefacts
Also found by: 0x1f8b, 0xYamiDancho, 0xf15ers, 0xkatana, ACai, AlleyCat, Bruhhh, Dravee, Funen, GimelSec, Hawkeye, IllIllI, MaratCerby, PPrieditis, Picodes, Ruhum, TerrierLover, VAD37, berndartmueller, csanuragjain, defsec, delfin454000, eccentricexit, ellahi, fatherOfBlocks, gzeon, hansfriese, hickuphh3, hyh, ilan, joestakey, juicy, kebabsec, oyc_109, rajatbeladiya, reassor, rfa, robee, samruna, simon135, sorrynotsorry, throttle
221.4454 DAI - $221.45
You allow in some arrays to have duplicates. Sometimes you assumes there are no duplicates in the array.
PermissionlessBasicPoolFactory.addPool pushed (rewardTokenAddresses)
The project is compiled with different versions of solidity, which is not recommended because it can lead to undefined behaviors.
owner param should be validated to make sure the owner address is not address(0). Otherwise if not given the right input all only owner accessible functions will be unaccessible.
VoterID.sol.createIdentityFor thisOwner VoterID.sol.setOwner newOwner
Users can mistakenly think that the return value is the named return, but it is actually the actualreturn statement that comes after. To know that the user needs to read the code and is confusing. Furthermore, removing either the actual return or the named return will save gas.
SpeedBumpPriceGate.sol, getCost MerkleEligibility.sol, addGate FixedPricePassThruGate.sol, getCost MerkleEligibility.sol, isEligible
The following contracts have a function that allows them an admin to change it to a different address. If the admin accidentally uses an invalid address for which they do not have the private key, then the system gets locked. It is important to have two steps admin change where the first is announcing a pending new admin and the new address should then claim its ownership. A similar issue was reported in a previous contest and was assigned a severity of medium: code-423n4/2021-06-realitycards-findings#105
VoterID.sol MerkleIdentity.sol
Those are functions and parameters pairs that the function doesn't use the parameter. In case those functions are external/public this is even worst since the user is required to put value that never used and can misslead him and waste its time.
VoterID.sol: function isContract parameter account isn't used. (isContract is internal)
The following functions are missing commenting as describe below:
MerkleIdentity.sol, verifyMetadata (public), @return is missing MerkleIdentity.sol, getPrice (public), parameter merkleIndex not commented
Transferring tokens to the zero address is usually prohibited to accidentally avoid "burning" tokens by sending them to an unrecoverable zero address.
https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleResistor.sol#L114 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleVesting.sol#L87 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/PermissionlessBasicPoolFactory.sol#L143 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleResistor.sol#L195 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/VoterID.sol#L142
Those are functions and parameters pairs that the function doesn't use the parameter. In case those functions are external/public this is even worst since the user is required to put value that never used and can misslead him and waste its time.
VoterID.sol: function isContract parameter account isn't used. (isContract is internal)
The following functions are missing commenting as describe below:
MerkleIdentity.sol, verifyMetadata (public), @return is missing MerkleIdentity.sol, getPrice (public), parameter merkleIndex not commented
To give more trust to users: functions that set key/critical variables should be put behind a timelock.
https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/VoterID.sol#L162 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleIdentity.sol#L75 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/PermissionlessBasicPoolFactory.sol#L314 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleIdentity.sol#L60 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/VoterID.sol#L251
Division by 0 can lead to accidentally revert, (An example of a similar issue - https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/84)
https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleVesting.sol#L117 startTime, endTime might be 0 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/SpeedBumpPriceGate.sol#L72 price might be 0 https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/MerkleResistor.sol#L264 coinsPerSecond might be 0
There are ERC20 tokens that charge fee for every transfer() / transferFrom().
Vault.sol#addValue() assumes that the received amount is the same as the transfer amount, and uses it to calculate attributions, balance amounts, etc. But, the actual transferred amount can be lower for those tokens. Therefore it's recommended to use the balance change before and after the transfer instead of the amount. This way you also support the tokens with transfer fee - that are popular.
https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/PermissionlessBasicPoolFactory.sol#L143
external / public functions parameters should be validated to make sure the address is not 0. Otherwise if not given the right input it can mistakenly lead to loss of user funds.
MerkleIdentity.sol.setTreeAdder newAdder MerkleIdentity.sol.addMerkleTree nftAddress MerkleIdentity.sol.addMerkleTree eligibilityAddress SpeedBumpPriceGate.sol.addGate beneficiary VoterID.sol.approve approved
#0 - illuzen
2022-05-10T08:51:05Z
Zero address checks are not in scope. Fee token issue is duplicate. Div by 0 issues are user mis-entering function parameters and are out of scope Timelock is partially duplicate, but wider in scope, valid Missing comment is valid Unused arguments is not valid, read the code Duplicates in array is not valid, tokens are not assumed to be unique Others are duplicates
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xYamiDancho, 0xf15ers, 0xkatana, ACai, CertoraInc, Dravee, Funen, GimelSec, Hawkeye, PPrieditis, Picodes, Ruhum, TerrierLover, Tomio, VAD37, Waze, csanuragjain, defsec, delfin454000, eccentricexit, ellahi, fatherOfBlocks, gzeon, hansfriese, horsefacts, ilan, joestakey, juicy, minhquanym, oyc_109, rajatbeladiya, reassor, rfa, robee, samruna, simon135, z3s
59.7678 DAI - $59.77
In the following files there are state variables that could be set immutable to save gas.
_name in VoterID.sol _minter in VoterID.sol _symbol in VoterID.sol globalBeneficiary in PermissionlessBasicPoolFactory.sol gateMaster in MerkleEligibility.sol
Caching the array length is more gas efficient. This is because access to a local variable in solidity is more efficient than query storage / calldata / memory. We recommend to change from:
for (uint256 i=0; i<array.length; i++) { ... }
to:
uint len = array.length for (uint256 i=0; i<len; i++) { ... }
PermissionlessBasicPoolFactory.sol, rewards, 224 MerkleLib.sol, proof, 22 PermissionlessBasicPoolFactory.sol, rewardTokens.pool, 266 PermissionlessBasicPoolFactory.sol, rewardFunding.pool, 141 PermissionlessBasicPoolFactory.sol, rewardTokenAddresses, 115
Prefix increments are cheaper than postfix increments.
Further more, using unchecked {++x} is even more gas efficient, and the gas saving accumulates every iteration and can make a real change
There is no risk of overflow caused by increamenting the iteration index in for loops (the ++i
in for (uint256 i = 0; i < numIterations; ++i)
).
But increments perform overflow checks that are not necessary in this case.
These functions use not using prefix increments (++x
) or not using the unchecked keyword:
change to prefix increment and unchecked: PermissionlessBasicPoolFactory.sol, i, 249 change to prefix increment and unchecked: PermissionlessBasicPoolFactory.sol, i, 168 change to prefix increment and unchecked: PermissionlessBasicPoolFactory.sol, i, 266 change to prefix increment and unchecked: PermissionlessBasicPoolFactory.sol, i, 115 change to prefix increment and unchecked: PermissionlessBasicPoolFactory.sol, i, 224
In for loops you initialize the index to start from 0, but it already initialized to 0 in default and this assignment cost gas. It is more clear and gas efficient to declare without assigning 0 and will have the same meaning:
PermissionlessBasicPoolFactory.sol, 249 MerkleLib.sol, 22 PermissionlessBasicPoolFactory.sol, 224 PermissionlessBasicPoolFactory.sol, 168 PermissionlessBasicPoolFactory.sol, 266
Reading a storage variable is gas costly (SLOAD). In cases of multiple read of a storage variable in the same scope, caching the first read (i.e saving as a local variable) can save gas and decrease the overall gas uses. The following is a list of functions and the storage variables that you read twice:
PermissionlessBasicPoolFactory.sol: globalBeneficiary is read twice in setGlobalTax MerkleIdentity.sol: treeAdder is read twice in addMerkleTree
Unnecessary default assignments, you can just declare and it will save gas and have the same meaning.
MerkleDropFactory.sol (L#17) : uint public numTrees = 0; MerkleEligibility.sol (L#31) : uint public numGates = 0; VoterID.sol (L#69) : uint public numIdentities = 0; MerkleVesting.sol (L#16) : uint public numTrees = 0; MerkleResistor.sol (L#24) : uint public numTrees = 0;
You can change the order of the storage variables to decrease memory uses.
In VoterID.sol,rearranging the storage fields can optimize to: 5 slots from: 6 slots. The new order of types (you choose the actual variables): 1. string 2. string 3. uint 4. address 5. bytes4 6. bytes4 7. bytes4 8. address 9. bytes4 10. bytes4
The following require messages are of length more than 32 and we think are short enough to short them into exactly 32 characters such that it will be placed in one slot of memory and the require function will cost less gas. The list:
Solidity file: MerkleDropFactory.sol, In line 90, Require message length to shorten: 35, The message: Provided merkle index doesn't exist Solidity file: PermissionlessBasicPoolFactory.sol, In line 315, Require message length to shorten: 34, The message: Only globalBeneficiary can set tax Solidity file: VoterID.sol, In line 239, Require message length to shorten: 35, The message: Identity: Not authorized to approve Solidity file: MerkleResistor.sol, In line 171, Require message length to shorten: 39, The message: You must initialize your account first. Solidity file: MerkleResistor.sol, In line 136, Require message length to shorten: 36, The message: Can only initialize your own tranche
You can use unchecked in the following calculations since there is no risk to overflow:
MerkleResistor.sol (L#264) - uint startTime = block.timestamp + vestingTime - (totalCoins / coinsPerSecond); MerkleResistor.sol (L#153) - tranches[destination][treeIndex] = Tranche( totalCoins, totalCoins, startTime, block.timestamp + vestingTime, coinsPerSecond, startTime );
The following functions are used exactly once. Therefore you can inline them and save gas and improve code clearness.
VoterID.sol, isContract VoterID.sol, checkOnERC721Received VoterID.sol, transfer
We recommend not to cache msg.sender since calling it is 2 gas while reading a variable is more.
https://github.com/code-423n4/2022-05-factorydao/tree/main/contracts/PermissionlessBasicPoolFactory.sol#L193
#0 - illuzen
2022-05-10T08:55:32Z
msg.sender cache: valid inline functions: valid unchecked: duplicate short messages: duplicate rearrange state: valid default assignment: duplicate storage double reading: valid all others duplicates