Platform: Code4rena
Start Date: 26/07/2022
Pot Size: $75,000 USDC
Total HM: 29
Participants: 179
Period: 6 days
Judge: LSDan
Total Solo HM: 6
Id: 148
League: ETH
Rank: 100/179
Findings: 4
Award: $56.64
π Selected for report: 0
π Solo Findings: 0
π Selected for report: cloudjunky
Also found by: 0x1f8b, 0x4non, 0x52, 0xDjango, 0xHarry, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 8olidity, Bnke0x0, CertoraInc, Chom, Deivitto, Dravee, GalloDaSballo, GimelSec, IllIllI, Jmaxmanblue, JohnSmith, Jujic, Kenshin, Krow10, Lambda, MEP, Noah3o6, RedOneN, Ruhum, StErMi, StyxRave, TomJ, Treasure-Seeker, TrungOre, _Adam, __141345__, arcoun, asutorufos, bardamu, bearonbike, bin2chen, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, cccz, codexploder, cryptonue, cryptphi, cthulhu_cult, dharma09, dipp, djxploit, durianSausage, ellahi, giovannidisiena, hansfriese, horsefacts, hyh, immeas, indijanc, jayjonah8, jayphbee, joestakey, kenzo, kyteg, ladboy233, minhquanym, navinavu, obront, oyc_109, peritoflores, rbserver, reassor, rokinot, rotcivegaf, saian, scaraven, shenwilly, simon135, sseefried, teddav, zzzitron
0.0037 USDC - $0.00
The use of the deprecated transfer()
function for an address will inevitably make the transaction fail when:
The claimer smart contract does not implement a payable function. The claimer smart contract does implement a payable fallback which uses more than 2300 gas unit. The claimer smart contract implements a payable fallback function that needs less than 2300 gas units but is called through proxy, raising the call's gas usage above 2300. Additionally, using higher than 2300 gas might be mandatory for some multisig wallets.
function payEther(uint256 payAmt, address payAddress) internal { // @audit transfer => call if (payAmt > 0) { // if royalty has to be paid payable(payAddress).transfer(payAmt); // royalty transfer to royaltyaddress } }
Manual.
I recommend using call()
instead of transfer()
#0 - KenzoAgada
2022-08-04T03:10:41Z
Duplicate of #343
π Selected for report: TomJ
Also found by: 0x4non, 0x52, 0xDjango, 0xNazgul, 0xf15ers, 0xsanson, 8olidity, Bnke0x0, CertoraInc, Ch_301, Chom, Dravee, GalloDaSballo, GimelSec, JC, Jujic, Kenshin, Kumpa, Lambda, M0ndoHEHE, PaludoX0, RedOneN, Ruhum, Sm4rty, Treasure-Seeker, TrungOre, Twpony, Waze, _Adam, __141345__, apostle0x01, arcoun, benbaessler, bin2chen, brgltd, cccz, cloudjunky, cryptonue, djxploit, ellahi, erictee, hansfriese, i0001, minhquanym, oyc_109, peritoflores, rbserver, reassor, rokinot, rotcivegaf, saian, shenwilly, sseefried
0.1513 USDC - $0.15
https://github.com/code-423n4/2022-07-golom/blob/main/contracts/core/GolomTrader.sol#L236
According to Openzeppelin's ERC721.sol::_safeTransfer()
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients * are aware of the ERC721 protocol to prevent tokens from being forever locked.
GolomTrader.sol::236 => ERC721(o.collection).transferFrom(o.signer, receiver, o.tokenId); GolomTrader.sol::301 => nftcontract.transferFrom(msg.sender, o.signer, o.tokenId); GolomTrader.sol::361 => nftcontract.transferFrom(msg.sender, o.signer, tokenId);
Manual.
Use OpenZeppelin's safeTransferFrom
to prevent NFT's from being lost.
#0 - KenzoAgada
2022-08-03T15:04:03Z
Duplicate of #342 But issue is very lacking in details
π Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0x52, 0xA5DF, 0xDjango, 0xLovesleep, 0xNazgul, 0xNineDec, 0xSmartContract, 0xackermann, 0xc0ffEE, 0xf15ers, 0xmatt, 0xsanson, 0xsolstars, 8olidity, AuditsAreUS, Bahurum, Bnke0x0, CRYP70, CertoraInc, Ch_301, Chom, CryptoMartian, Deivitto, DevABDee, Dravee, ElKu, Franfran, Funen, GalloDaSballo, GimelSec, GiveMeTestEther, Green, JC, Jmaxmanblue, JohnSmith, Jujic, Junnon, Kenshin, Krow10, Kumpa, Lambda, MEP, Maxime, MiloTruck, Mohandes, NoamYakov, Picodes, RedOneN, Rohan16, Rolezn, Ruhum, RustyRabbit, Sm4rty, Soosh, StErMi, StyxRave, Tadashi, TomJ, Treasure-Seeker, TrungOre, Waze, _Adam, __141345__, ajtra, ak1, apostle0x01, arcoun, asutorufos, async, benbaessler, berndartmueller, bin2chen, brgltd, c3phas, cRat1st0s, carlitox477, chatch, codetilda, codexploder, cryptonue, cryptphi, csanuragjain, cthulhu_cult, delfin454000, dipp, dirk_y, djxploit, ellahi, exd0tpy, fatherOfBlocks, giovannidisiena, hansfriese, horsefacts, hyh, idkwhatimdoing, indijanc, jayfromthe13th, jayphbee, joestakey, kenzo, kyteg, lucacez, luckypanda, mics, minhquanym, obront, oyc_109, pedr02b2, rajatbeladiya, rbserver, reassor, robee, rokinot, rotcivegaf, sach1r0, saian, saneryee, sashik_eth, scaraven, shenwilly, simon135, sseefried, supernova, teddav, ych18, zuhaibmohd, zzzitron
35.1687 USDC - $35.17
startTime
The RewardDistributor
contract uses a hardcoded startTime
which starts before the audit ends.
startTime = 1659211200; // @audit starts july 30th but audit ends aug 1st
Use the intended start time.
The return value of an external transfer
/transferFrom
call is not checked
GolomTrader.sol::382 => WETH.transferFrom(o.signer, address(this), o.totalAmt * amount); RewardDistributor.sol::151 => rewardToken.transfer(addr, reward); RewardDistributor.sol::165 => rewardToken.transfer(addr, reward); RewardDistributor.sol::208 => rewardToken.transfer(tokenowner, reward); RewardDistributor.sol::209 => weth.transfer(tokenowner, rewardEth); VoteEscrowCore.sol::861 => assert(IERC20(token).transferFrom(from, address(this), _value)); VoteEscrowCore.sol::1023 => assert(IERC20(token).transfer(msg.sender, value));
Use SafeERC20
, or ensure that the transfer
/transferFrom
return value is checked.
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.
GolomToken.sol::2 => pragma solidity ^0.8.11; GovernerBravo.sol::2 => pragma solidity ^0.8.11;
Avoid floating pragmas for non-library contracts. It is recommended to pin to a concrete compiler version.
Timlock.sol => Timelock.sol GovernerBravo.sol => GovernorBravo.sol GovernerBravo.sol but the contract name is GovernorAlpha
manual, slither
π Selected for report: JohnSmith
Also found by: 0x1f8b, 0xA5DF, 0xDjango, 0xKitsune, 0xLovesleep, 0xNazgul, 0xSmartContract, 0xmatt, 0xsam, Aymen0909, Bnke0x0, CRYP70, Chandr, Chinmay, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, Funen, GalloDaSballo, Green, IllIllI, JC, Jmaxmanblue, Junnon, Kaiziron, Kenshin, Krow10, Maxime, Migue, MiloTruck, Noah3o6, NoamYakov, Randyyy, RedOneN, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StyxRave, TomJ, Tomio, _Adam, __141345__, ajtra, ak1, apostle0x01, asutorufos, async, benbaessler, brgltd, c3phas, cRat1st0s, carlitox477, delfin454000, djxploit, durianSausage, ellahi, erictee, fatherOfBlocks, gerdusx, gogo, hyh, jayfromthe13th, jayphbee, joestakey, kaden, kenzo, kyteg, ladboy233, lucacez, m_Rassska, mics, minhquanym, oyc_109, pfapostol, rbserver, reassor, rfa, robee, rokinot, sach1r0, saian, samruna, sashik_eth, simon135, supernova, tofunmi, zuhaibmohd
21.3211 USDC - $21.32
import hardhat/console.sol
before deploymentWaste of gas.
import 'hardhat/console.sol';
Remove the import.
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack. Caching the array length in the stack saves around 3 gas per iteration.
GolomTrader.sol::415 => for (uint256 i = 0; i < proof.length; i++) { GovernerBravo.sol::240 => for (uint256 i = 0; i < proposal.targets.length; i++) { GovernerBravo.sol::269 => for (uint256 i = 0; i < proposal.targets.length; i++) { GovernerBravo.sol::293 => for (uint256 i = 0; i < proposal.targets.length; i++) { RewardDistributor.sol::143 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::157 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::180 => for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { RewardDistributor.sol::183 => for (uint256 index = 0; index < epochs.length; index++) {
Store the arrayβs length in a variable before the for-loop.
!= 0
instead of > 0
for Unsigned Integer Comparison in require statements.!= 0
is cheapear than > 0
when comparing unsigned integers in require statements.
GovernerBravo.sol::325 => require(proposalCount >= proposalId && proposalId > 0, 'GovernorAlpha::state: invalid proposal id'); VoteEscrowCore.sol::927 => require(_value > 0); // dev: need non-zero value VoteEscrowCore.sol::944 => require(_value > 0); // dev: need non-zero value
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)
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.
GolomTrader.sol::415 => for (uint256 i = 0; i < proof.length; i++) { GovernerBravo.sol::240 => for (uint256 i = 0; i < proposal.targets.length; i++) { GovernerBravo.sol::269 => for (uint256 i = 0; i < proposal.targets.length; i++) { GovernerBravo.sol::293 => for (uint256 i = 0; i < proposal.targets.length; i++) { RewardDistributor.sol::142 => uint256 reward = 0; RewardDistributor.sol::143 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::156 => uint256 reward = 0; RewardDistributor.sol::157 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::175 => uint256 reward = 0; RewardDistributor.sol::176 => uint256 rewardEth = 0; RewardDistributor.sol::180 => for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { RewardDistributor.sol::183 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::222 => uint256 reward = 0; RewardDistributor.sol::223 => uint256 rewardEth = 0; RewardDistributor.sol::226 => for (uint256 index = 0; index < epoch; index++) { RewardDistributor.sol::257 => uint256 reward = 0; RewardDistributor.sol::258 => for (uint256 index = 0; index < epoch; index++) { RewardDistributor.sol::272 => uint256 reward = 0; RewardDistributor.sol::273 => for (uint256 index = 0; index < epoch; index++) { VoteEscrowCore.sol::745 => for (uint256 i = 0; i < 255; ++i) { VoteEscrowCore.sol::1044 => for (uint256 i = 0; i < 128; ++i) { VoteEscrowCore.sol::1115 => for (uint256 i = 0; i < 128; ++i) { VoteEscrowCore.sol::1167 => for (uint256 i = 0; i < 255; ++i) { VoteEscrowCore.sol::1211 => uint256 dt = 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.
GolomTrader.sol::415 => for (uint256 i = 0; i < proof.length; i++) { GovernerBravo.sol::240 => for (uint256 i = 0; i < proposal.targets.length; i++) { GovernerBravo.sol::269 => for (uint256 i = 0; i < proposal.targets.length; i++) { GovernerBravo.sol::293 => for (uint256 i = 0; i < proposal.targets.length; i++) { RewardDistributor.sol::143 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::157 => for (uint256 index = 0; index < epochs.length; index++) { RewardDistributor.sol::180 => for (uint256 tindex = 0; tindex < tokenids.length; tindex++) { RewardDistributor.sol::183 => for (uint256 index = 0; index < epochs.length; index++) {
Use ++i
instead of i++
to increment the value of an uint variable.
Same thing for --i
and i--
.
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.
VoteEscrowCore.sol::1049 => uint256 _mid = (_min + _max + 1) / 2; VoteEscrowCore.sol::1120 => 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;
c4udit, manual, slither