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: 33/71
Findings: 3
Award: $154.12
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MaratCerby
Also found by: CertoraInc, Ruhum, VAD37, berndartmueller, broccolirob, cryptphi, danb, gzeon, horsefacts, hyh, joestakey, leastwood, pedroais, peritoflores, throttle, wuwe1, z3s
19.1789 DAI - $19.18
the ERC20 .transfer()
method is used in MerkleVesting.sol
, but its return value is not checked.
It is good to add a require() statement that checks the return value of token transfers, or to use something like OpenZeppelin’s safeTransfer. Failure to do so will cause silent failures of transfers, which will be confusing for users calling these functions, and may result in loss of funds.
Medium
Instances include:
MerkleVesting:173: IERC20(tree.tokenAddress).transfer(destination, currentWithdrawal);
If the transfer fails, the function will not revert. As the tree balance is updated line 167, this would result in a user not being able to retrieve their tokens.
Manual Analysis
use safeTransfer
or require()
consistently.
#0 - illuzen
2022-05-12T05:18:35Z
Duplicate #27
🌟 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
75.1651 DAI - $75.17
Few vulnerabilities were found examining the contracts. The main concerns are with the lack of zero-address check in some setters.
When there are mappings that use the same key value, having separate fields is error prone, for instance in case of deletion or with future new fields.
Non-Critical
Instances include:
PermissionlessBasicPoolFactory.sol:57: mapping (uint => Pool) public pools; PermissionlessBasicPoolFactory.sol:59: mapping (uint => Metadata) public metadatas; PermissionlessBasicPoolFactory.sol:61: mapping (uint => uint[]) public taxes;
Manual Analysis
Group the related data in a struct and use one mapping. For instance, the mitigation could be:
struct PoolStruct { Pool pool; Metadata metadata; uint[] tax; }
And it would be used as a state variable:
mapping(uint256 => PoolStruct) pool;
There are a few spelling mistakes in the contracts.
Non-Critical
Instances include:
MerkleDropFactory.sol:76: //effect
MerkleEligibility.sol:11: //effect
MerkleResistor.sol:200,201: //effect
MerkleVesting.sol:88: //effect MerkleVesting.sol:170: //effect
PermissionlessBasicPoolFactory.sol:82: //effect
Manual Analysis
affect
is the correct verb to use in these instances.
uint
is an alias for uint256
.
It is better to use uint256: it brings readability and consistency in the code, and it future proofs it in case of any changes to the alias of uint
Non-Critical
Instances include:
All the contracts in scope, except for VoterID.sol
where uint256
is used
Manual Analysis
replace uint
with
uint256
There should be a non-zero (address or integer) check in all setters.
Low
in MerkleIdentity.sol
, setManagement()
changes the management address to the one passed as an argument, but there is no zero address check. If it were to be accidentally set to the address zero, functions with the modifier managementOnly
could not be called anymore, in particular no new trees could be added to the contract.
Manual Analysis
Add zero-address checks in setters.
All setters should emit an event, so the Dapps can detect important changes
Low
Instances include:
MerkleIdentity.sol:60:setManagement(address newMgmt) MerkleIdentity.sol:67:setTreeAdder(address newAdder) MerkleIdentity.sol:75:setIpfsHash(uint merkleIndex, bytes32 hash)
Manual Analysis
Emit an event in every setter;
#0 - illuzen
2022-05-12T08:59:03Z
all duplicates
#1 - gititGoro
2022-06-12T03:07:10Z
For item 1, common code practices from other languages can be abandoned in the presence of a gas constrained environment. Since uint is an alias for uint256, it follows that the use of uint isn't ambiguous in code.
🌟 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
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
Instances include:
scope: addMerkleTree()
numTrees
is read twiceMerkleDropFactory.sol:59 MerkleDropFactory.sol:61
scope: addGate()
numGates
is read twiceMerkleEligibility.sol:49 MerkleEligibility.sol:50
scope: addMerkleTree()
numTrees
is read twiceMerkleResistor.sol:99 MerkleResistor.sol:100
scope: addMerkleRoot()
numTrees
is read twiceMerkleVesting.sol:71 MerkleVesting.sol:72
Manual Analysis
cache these storage variables in memory
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
Instances include:
scope: withdraw()
MerkleDropFactory.sol:88: bytes32[] memory proof
scope: isEligible()
MerkleEligibility.sol:70: bytes32[] memory proof
scope: passThruGate()
MerkleEligibility.sol:85: bytes32[] memory proof
scope: withdraw()
MerkleIdentity.sol:116: string memory uri, bytes32[] memory addressProof, bytes32[] memory metadataProof
scope: isEligible()
MerkleIdentity.sol:152: bytes32[] memory proof
scope: verifyMetadata()
MerkleIdentity.sol:163: string memory uri, bytes32[] memory proof
scope: verifyProof()
MerkleLib.sol:17: bytes32[] memory proof
scope: initialize()
MerkleResistor.sol:134: bytes32[] memory proof
scope: initialize()
MerkleVesting.sol:104: bytes32[] memory proof
scope: addPool()
PermissionlessBasicPoolFactory.sol:95: uint[] memory rewardsWeiPerSecondPerToken PermissionlessBasicPoolFactory.sol:99: address[] memory rewardTokenAddresses
scope: createIdentityFor()
VoterID.sol:122: string memory uri
scope: setTokenURI()
VoterID.sol:162: string memory uri
scope: safeTransferFrom()
VoterID.sol:215: bytes memory data
Manual Analysis
Replace memory
with calldata
In the EVM, there is no opcode for >=
or <=
.
When using greater than or equal, two operations are performed: >
and =
.
Using strict comparison operators hence saves gas
Instances include:
FixedPricePassThruGate.sol:48
MerkleDropFactory.sol:90
MerkleIdentity.sol:124
MerkleResistor.sol:179
MerkleVesting.sol:153
SpeedBumpPriceGate.sol:67
Manual Analysis
Replace <=
with <
, and >=
with >
. Do not forget to increment/decrement the compared variable
However, if 1
is negligible compared to the value of the variable, we can omit the increment.
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters.
Instances include:
MerkleEligibility.sol:35 address _gateMaster
MerkleIdentity.sol:52 address _mgmt
PermissionlessBasicPoolFactory.sol:75 address _globalBeneficiary, uint _globalTaxPerCapita
VoterID.sol:108 address ooner, address minter, string memory nomen, string memory symbowl
Manual Analysis
Hardcode the state variable with its initial value instead of writing it during contract deployment with constructor parameters.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here
Custom errors are defined using the error statement
Instances include:
FixedPricePassThruGate.sol:48 FixedPricePassThruGate.sol:54
MerkleDropFactory.sol:77 MerkleDropFactory.sol:90 MerkleDropFactory.sol:92 MerkleDropFactory.sol:98 MerkleDropFactory.sol:107
MerkleEligibility.sol:86 MerkleEligibility.sol:89
MerkleIdentity.sol:97 MerkleIdentity.sol:124 MerkleIdentity.sol:127
MerkleResistor.sol:82 MerkleResistor.sol:83 MerkleResistor.sol:121 MerkleResistor.sol:136 MerkleResistor.sol:138 MerkleResistor.sol:144 MerkleResistor.sol:149 MerkleResistor.sol:171 MerkleResistor.sol:175 MerkleResistor.sol:204
MerkleVesting.sol:89 MerkleVesting.sol:106 MerkleVesting.sol:113 MerkleVesting.sol:141 MerkleVesting.sol:145 MerkleVesting.sol:147
PermissionlessBasicPoolFactory.sol:112 PermissionlessBasicPoolFactory.sol:148 PermissionlessBasicPoolFactory.sol:159 PermissionlessBasicPoolFactory.sol:160 PermissionlessBasicPoolFactory.sol:182 PermissionlessBasicPoolFactory.sol:183 PermissionlessBasicPoolFactory.sol:184 PermissionlessBasicPoolFactory.sol:185 PermissionlessBasicPoolFactory.sol:199 PermissionlessBasicPoolFactory.sol:211 PermissionlessBasicPoolFactory.sol:213 PermissionlessBasicPoolFactory.sol:214 PermissionlessBasicPoolFactory.sol:215 PermissionlessBasicPoolFactory.sol:234 PermissionlessBasicPoolFactory.sol:244 PermissionlessBasicPoolFactory.sol:245 PermissionlessBasicPoolFactory.sol:246 PermissionlessBasicPoolFactory.sol:254 PermissionlessBasicPoolFactory.sol:263 PermissionlessBasicPoolFactory.sol:271 PermissionlessBasicPoolFactory.sol:315 PermissionlessBasicPoolFactory.sol:316
SpeedBumpPriceGate.sol:67 SpeedBumpPriceGate.sol:80
VoterID.sol:123 VoterID.sol:124 VoterID.sol:184 VoterID.sol:199 VoterID.sol:217 VoterID.sol:239 VoterID.sol:240 VoterID.sol:262 VoterID.sol:305 VoterID.sol:306 VoterID.sol:437 VoterID.sol:449 VoterID.sol:450
Manual Analysis
Replace require and revert statements with custom errors.
For instance, in MerkleEligibility.sol
:
Replace
require(msg.sender == gateMaster, "Only gatemaster may call this.");
with
if (msg.sender != gateMaster) { revert IsNotGateMaster(msg.sender); }
and define the custom error in the contract
error IsNotGateMaster(address _address);
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
Instances include:
MerkleDropFactory.sol:17: uint public numTrees = 0;
MerkleEligibility.sol:31: uint public numGates = 0;
MerkleLib.sol:22: uint i = 0;
MerkleResistor.sol:24: uint public numTrees = 0; MerkleResistor.sol:176: uint currentWithdrawal = 0;
MerkleVesting.sol:16: uint public numTrees = 0; MerkleVesting.sol:150: uint currentWithdrawal = 0;
PermissionlessBasicPoolFactory.sol:115: uint i = 0; PermissionlessBasicPoolFactory.sol:141: uint i = 0; PermissionlessBasicPoolFactory.sol:168: uint i = 0; PermissionlessBasicPoolFactory.sol:224: uint i = 0; PermissionlessBasicPoolFactory.sol:249: uint i = 0; PermissionlessBasicPoolFactory.sol:266: uint i = 0;
VoterID.sol:69: uint public numIdentities = 0;
Manual Analysis
Remove explicit initialization for default values.
When emitting an event, using a local variable instead of a storage variable saves gas.
Instances include:
MerkleDropFactory.sol:61
MerkleResistor.sol:122
MerkleVesting.sol:72
Manual Analysis
The storage variable is read multiple times in the function and it is recommended to cache it into memory, then passing the cached variable in the emit
statement, as explained in the cache paragraph
Prefix increments are cheaper than postfix increments.
Instances include:
MerkleLib.sol:22 i += 1
PermissionlessBasicPoolFactory.sol:115 i++ PermissionlessBasicPoolFactory.sol:141 i++ PermissionlessBasicPoolFactory.sol:168 i++ PermissionlessBasicPoolFactory.sol:224 i++ PermissionlessBasicPoolFactory.sol:249 i++ PermissionlessBasicPoolFactory.sol:266 i++
Manual Analysis
change variable++
to ++variable
, and variable += 1
to ++variable
.
When emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.
Instances include:
VoterID.sol:154
Manual Analysis
Replace
address oldOwner = _owner_; _owner_ = newOwner; emit OwnerUpdated(oldOwner, newOwner);
with
emit OwnerUpdated(_owner_, newOwner); _owner_ = newOwner;
#0 - illuzen
2022-05-12T08:58:38Z
all duplicates