Platform: Code4rena
Start Date: 12/08/2022
Pot Size: $35,000 USDC
Total HM: 10
Participants: 126
Period: 3 days
Judge: Justin Goro
Total Solo HM: 3
Id: 154
League: ETH
Rank: 51/126
Findings: 2
Award: $48.34
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: oyc_109
Also found by: 0x1f8b, 0x52, 0xDjango, 0xLovesleep, 0xNazgul, 0xNineDec, 0xbepresent, 0xmatt, 0xsolstars, Aymen0909, Bahurum, Bnke0x0, CertoraInc, Chom, CodingNameKiki, DecorativePineapple, Deivitto, Dravee, ElKu, Funen, GalloDaSballo, IllIllI, JC, JohnSmith, Junnon, KIntern_NA, Lambda, LeoS, MiloTruck, Noah3o6, PaludoX0, RedOneN, Respx, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Sm4rty, TomJ, Vexjon, Waze, Yiko, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, auditor0517, bin2chen, bobirichman, brgltd, bulej93, byndooa, c3phas, cRat1st0s, cryptphi, csanuragjain, d3e4, defsec, delfin454000, djxploit, durianSausage, ellahi, erictee, exd0tpy, fatherOfBlocks, gogo, jonatascm, ladboy233, medikko, mics, natzuu, neumo, p_crypt0, paribus, pfapostol, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, sach1r0, saneryee, seyni, sikorico, simon135, sseefried, wagmi, wastewa
33.3886 USDC - $33.39
Low
address(0)
when assigning values to address state variables.Non-Critical
Contracts implementing access control's, e.g. owner
, should consider implementing a Two-Step Transfer pattern.
Otherwise it's possible that the role mistakenly transfers ownership to the wrong address, resulting in a loss of the role
function transferOwnership(address _addr) external { require(msg.sender == owner, "Only owner"); owner = _addr; emit TransferOwnership(_addr); }
Consider adding a pendingOwner
where the new owner will have to accept the ownership.
Example
address owner; address pendingOwner; // ... function setPendingOwner(address newPendingOwner) external { require(msg.sender == owner, "!owner"); emit NewPendingOwner(newPendingOwner); pendingOwner = newPendingOwner; } function acceptOwnership() external { require(msg.sender == pendingOwner, "!pendingOwner"); emit NewOwner(pendingOwner); owner = pendingOwner; pendingOwner = address(0); }
The return value of an external transfer
/transferFrom
call is not checked
VotingEscrow.sol::426 => token.transferFrom(msg.sender, address(this), _value), VotingEscrow.sol::486 => token.transferFrom(msg.sender, address(this), _value),
Use SafeERC20
, or ensure that the transferFrom
return value is checked.
address(0)
when assigning values to address state variables.Setters of address type parameters should include a zero-address check otherwise contract functionality may become inaccessible or tokens burnt forever.
VotingEscrow.sol::139 => function transferOwnership(address _addr) external { VotingEscrow.sol::146 => function updateBlocklist(address _addr) external { VotingEscrow.sol::152 => function updatePenaltyRecipient(address _addr) external { Blocklist.sol::14 => constructor(address _manager, address _ve) {
constructor( address _owner, address _penaltyRecipient, address _token, string memory _name, string memory _symbol ) {
Add address(0)
checks.
A known vulnerable compiler version may accidentally be selected or security tools might fall-back to an older compiler version ending up checking a different EVM compilation that is ultimately deployed on the blockchain.
VotingEscrow.sol::2 => pragma solidity ^0.8.3; Blocklist.sol::2 => pragma solidity ^0.8.3; IBlocklist.sol::2 => pragma solidity ^0.8.3; IERC20.sol::2 => pragma solidity ^0.8.3; IVotingEscrow.sol::2 => pragma solidity ^0.8.3;
Avoid floating pragmas for non-library contracts. It is recommended to pin to a concrete compiler version.
constant
's should be defined rather than using magic numbers.VotingEscrow.sol::653 => uint256 penaltyAmount = (value * penaltyRate) / 10**18
- VotingEscrow.sol::705 preceeding + VotingEscrow.sol::705 => preceding - VotingEscrow.sol::729 preceeding + VotingEscrow.sol::729 => preceding
🌟 Selected for report: IllIllI
Also found by: 0x040, 0x1f8b, 0xDjango, 0xHarry, 0xLovesleep, 0xNazgul, 0xNineDec, 0xSmartContract, 0xackermann, 0xbepresent, 2997ms, Amithuddar, Aymen0909, Bnke0x0, CRYP70, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, Funen, GalloDaSballo, JC, JohnSmith, Junnon, LeoS, Metatron, MiloTruck, Noah3o6, NoamYakov, PaludoX0, RedOneN, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, SooYa, SpaceCake, TomJ, Tomio, Waze, Yiko, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, bobirichman, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, chrisdior4, csanuragjain, d3e4, defsec, delfin454000, djxploit, durianSausage, ellahi, erictee, fatherOfBlocks, gerdusx, gogo, ignacio, jag, ladboy233, m_Rassska, medikko, mics, natzuu, newfork01, oyc_109, paribus, pfapostol, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, sach1r0, saian, sashik_eth, sikorico, simon135
14.9459 USDC - $14.95
Gas
!= 0
instead of > 0
for Unsigned Integer Comparison in require statements++i
costs less gas compared to i++
or i += 1
storage
instead of memory
for structs/arrays.x += y
costs more gas than x = x + y
for state variables.!= 0
instead of > 0
for Unsigned Integer Comparison in require statements!= 0
is cheapear than > 0
when comparing unsigned integers in require statements.
VotingEscrow.sol::412 => require(_value > 0, "Only non zero amount"); VotingEscrow.sol::448 => require(_value > 0, "Only non zero amount");
Use != 0
instead of > 0
.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
VotingEscrow.sol::116 => require(decimals <= 18, "Exceeds max decimals"); VotingEscrow.sol::140 => require(msg.sender == owner, "Only owner"); VotingEscrow.sol::147 => require(msg.sender == owner, "Only owner"); VotingEscrow.sol::154 => require(msg.sender == owner, "Only owner"); VotingEscrow.sol::162 => require(msg.sender == owner, "Only owner"); VotingEscrow.sol::171 => require(msg.sender == blocklist, "Only Blocklist"); VotingEscrow.sol::412 => require(_value > 0, "Only non zero amount"); VotingEscrow.sol::413 => require(locked_.amount == 0, "Lock exists"); VotingEscrow.sol::414 => require(unlock_time >= locked_.end, "Only increase lock end"); // from using quitLock, user should increaseAmount instead VotingEscrow.sol::415 => require(unlock_time > block.timestamp, "Only future lock end"); VotingEscrow.sol::416 => require(unlock_time <= block.timestamp + MAXTIME, "Exceeds maxtime"); VotingEscrow.sol::448 => require(_value > 0, "Only non zero amount"); VotingEscrow.sol::449 => require(locked_.amount > 0, "No lock"); VotingEscrow.sol::450 => require(locked_.end > block.timestamp, "Lock expired"); VotingEscrow.sol::469 => require(locked_.amount > 0, "Delegatee has no lock"); VotingEscrow.sol::470 => require(locked_.end > block.timestamp, "Delegatee lock expired"); VotingEscrow.sol::502 => require(locked_.amount > 0, "No lock"); VotingEscrow.sol::503 => require(unlock_time > locked_.end, "Only increase lock end"); VotingEscrow.sol::504 => require(unlock_time <= block.timestamp + MAXTIME, "Exceeds maxtime"); VotingEscrow.sol::511 => require(oldUnlockTime > block.timestamp, "Lock expired"); VotingEscrow.sol::529 => require(locked_.amount > 0, "No lock"); VotingEscrow.sol::530 => require(locked_.end <= block.timestamp, "Lock not expired"); VotingEscrow.sol::531 => require(locked_.delegatee == msg.sender, "Lock delegated"); VotingEscrow.sol::546 => require(token.transfer(msg.sender, value), "Transfer failed"); VotingEscrow.sol::563 => require(!IBlocklist(blocklist).isBlocked(_addr), "Blocked contract"); VotingEscrow.sol::564 => require(locked_.amount > 0, "No lock"); VotingEscrow.sol::565 => require(locked_.delegatee != _addr, "Already delegated"); VotingEscrow.sol::587 => require(toLocked.amount > 0, "Delegatee has no lock"); VotingEscrow.sol::588 => require(toLocked.end > block.timestamp, "Delegatee lock expired"); VotingEscrow.sol::589 => require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); VotingEscrow.sol::635 => require(locked_.amount > 0, "No lock"); VotingEscrow.sol::636 => require(locked_.end > block.timestamp, "Lock expired"); VotingEscrow.sol::637 => require(locked_.delegatee == msg.sender, "Lock delegated"); VotingEscrow.sol::657 => require(token.transfer(msg.sender, remainingAmount), "Transfer failed"); VotingEscrow.sol::676 => require(token.transfer(penaltyRecipient, amount), "Transfer failed"); VotingEscrow.sol::776 => require(_blockNumber <= block.number, "Only past block number"); VotingEscrow.sol::877 => require(_blockNumber <= block.number, "Only past block number"); Blocklist.sol::24 => require(msg.sender == manager, "Only manager"); Blocklist.sol::25 => require(_isContract(addr), "Only contracts");
Use custom errors instead of revert strings.
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.
VotingEscrow.sol::229 => int128 oldSlopeDelta = 0; VotingEscrow.sol::230 => int128 newSlopeDelta = 0; VotingEscrow.sol::298 => uint256 blockSlope = 0; // dblock/dt VotingEscrow.sol::309 => for (uint256 i = 0; i < 255; i++) { VotingEscrow.sol::313 => int128 dSlope = 0; VotingEscrow.sol::714 => uint256 min = 0; VotingEscrow.sol::717 => for (uint256 i = 0; i < 128; i++) { VotingEscrow.sol::737 => uint256 min = 0; VotingEscrow.sol::739 => for (uint256 i = 0; i < 128; i++) { VotingEscrow.sol::793 => uint256 dBlock = 0; VotingEscrow.sol::794 => uint256 dTime = 0; VotingEscrow.sol::834 => for (uint256 i = 0; i < 255; i++) { VotingEscrow.sol::836 => int128 dSlope = 0; VotingEscrow.sol::889 => uint256 dTime = 0;
Remove explicit default initializations.
++i
costs less gas compared to i++
or i += 1
++i
costs less gas compared to i++
or i += 1
for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration). This statement is true even with the optimizer enabled.
VotingEscrow.sol::309 => for (uint256 i = 0; i < 255; i++) { VotingEscrow.sol::717 => for (uint256 i = 0; i < 128; i++) { VotingEscrow.sol::739 => for (uint256 i = 0; i < 128; i++) { VotingEscrow.sol::834 => for (uint256 i = 0; i < 255; i++) {
Use ++i
instead of i++
to increment the value of an uint variable.
A division/multiplication by any number x
being a power of 2 can be calculated by shifting log2(x)
to the right/left. While the DIV
opcode uses 5 gas, the SHR
opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.
VotingEscrow.sol::719 => uint256 mid = (min + max + 1) / 2; VotingEscrow.sol::743 => uint256 mid = (min + max + 1) / 2;
Use SHR/SHL. Bad
uint256 b = a / 2 uint256 c = a / 4; uint256 d = a * 8;
Good
uint256 b = a >> 1; uint256 c = a >> 2; uint256 d = a << 3;
Contracts in scope use pragma solidity ^0.8.3
allowing a wide enough range of versions.
VotingEscrow.sol::2 => pragma solidity ^0.8.3; Blocklist.sol::2 => pragma solidity ^0.8.3; IBlocklist.sol::2 => pragma solidity ^0.8.3; IERC20.sol::2 => pragma solidity ^0.8.3; IVotingEscrow.sol::2 => pragma solidity ^0.8.3;
Consider locking compiler version, for example pragma solidity 0.8.6
. This can have additional benefits, for example using custom errors to save gas and so forth.
storage
instead of memory
for structs/arrays.When 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.
VotingEscrow.sol::172 => LockedBalance memory locked_ = locked[_addr]; VotingEscrow.sol::214 => Point memory point = userPointHistory[_addr][uepoch]; VotingEscrow.sol::410 => LockedBalance memory locked_ = locked[msg.sender]; VotingEscrow.sol::446 => LockedBalance memory locked_ = locked[msg.sender]; VotingEscrow.sol::499 => LockedBalance memory locked_ = locked[msg.sender]; VotingEscrow.sol::527 => LockedBalance memory locked_ = locked[msg.sender]; VotingEscrow.sol::561 => LockedBalance memory locked_ = locked[msg.sender]; VotingEscrow.sol::633 => LockedBalance memory locked_ = locked[msg.sender]; VotingEscrow.sol::759 => Point memory lastPoint = userPointHistory[_owner][epoch]; VotingEscrow.sol::783 => Point memory upoint = userPointHistory[_owner][userEpoch]; VotingEscrow.sol::788 => Point memory point0 = pointHistory[epoch]; VotingEscrow.sol::796 => Point memory point1 = pointHistory[epoch + 1]; VotingEscrow.sol::866 => Point memory lastPoint = pointHistory[epoch_]; VotingEscrow.sol::882 => Point memory point = pointHistory[targetEpoch]; VotingEscrow.sol::891 => Point memory pointNext = pointHistory[targetEpoch + 1];
Use storage
instead of memory
for findings above
x += y
costs more gas than x = x + y
for state variables.Same thing applies for subtraction
VotingEscrow.sol::418 => locked_.amount += int128(int256(_value)); VotingEscrow.sol::420 => locked_.delegated += int128(int256(_value)); VotingEscrow.sol::460 => newLocked.amount += int128(int256(_value)); VotingEscrow.sol::461 => newLocked.delegated += int128(int256(_value)); VotingEscrow.sol::465 => locked_.amount += int128(int256(_value)); VotingEscrow.sol::472 => newLocked.delegated += int128(int256(_value)); VotingEscrow.sol::537 => newLocked.delegated -= int128(int256(value)); VotingEscrow.sol::603 => newLocked.delegated += value; VotingEscrow.sol::612 => newLocked.delegated -= value; VotingEscrow.sol::642 => newLocked.delegated -= int128(int256(value)); VotingEscrow.sol::654 => penaltyAccumulated += penaltyAmount;
Use x = x + y
instead of `x += y
manual, c4udit, slither