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: 31/179
Findings: 5
Award: $396.78
๐ 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
Judge has assessed an item in Issue #236 as Medium risk. The relevant finding follows:
fillAsk
When a user fills an ask order by calling fillAsk
, the ERC721.transferFrom
method is used to transfer the NFT to the receiver
. Should the receiver
be a smart contract that is unable to handle ERC721 tokens, this would result in the NFT being locked forever.
Low
Instances include:
Manual Analysis
Consider using a safeTransferFrom
method. It is a common approach to avoid using this sort of method to avoid reentrancy, but as it is not mentioned in the readme, this issue is still worth raising.
#0 - dmvt
2022-10-21T14:49:34Z
Solidity ecrecover
is used in the validateOrder
function, but not there is no check that the return value is not zero.
If the order maker, ie o.signer
, is set as zero, fake orders can pass the validateOrder
checks.
Medium
If you supply an order with invalid o.v, o.r, o.s
, ecrecover
will return zero, making address signaturesigner = ecrecover(hash, o.v, o.r, o.s)
as the zero address.
This means the check in the next line will pass for o.signer = address(0)
, meaning validateOrder
passes for fake orders set with o.signer = address(0)
Manual Analysis
Revert if ecrecover
returns 0x0
. Consider also filtering on the Front-end, preventing orders with 0x0 makers.
#0 - KenzoAgada
2022-08-05T02:02:02Z
Duplicate of #357
๐ Selected for report: AuditsAreUS
Also found by: 0xSky, CertoraInc, GimelSec, GiveMeTestEther, Green, Lambda, Ruhum, RustyRabbit, Treasure-Seeker, Twpony, arcoun, bin2chen, cccz, codexploder, cryptonue, dipp, horsefacts, jayphbee, joestakey, minhquanym, obront, peritoflores, rbserver, reassor, rotcivegaf, scaraven, ych18
4.5163 USDC - $4.52
When users fill an ask order by calling fillAsk
, the function checks that the msg.value
is greater than or equal to the required amount.
All the subsequent payments (fees, exchange share, pre payment, order maker) are made based on o.totalAmt
, not msg.value
. If the amount sent is more than required, there is no way for a user to retrieve this amount.
Medium
Assume a user calling fillAsk
and setting msg.value
so that it is strictly greater than o.totalAmt * amount + p.paymentAmt
. The payments are done but the difference between msg.value
and o.totalAmt * amount + p.paymentAmt
is kept in GolomTrader
.
As there is no withdrawal function in this contract, this amount is locked forever.
Manual Analysis
+ 217: require(msg.value == o.totalAmt * amount + p.paymentAmt, 'mgmtm') - 217: require(msg.value >= o.totalAmt * amount + p.paymentAmt, 'mgmtm')
Another option is to refund the ETH surplus to the user.
#0 - KenzoAgada
2022-08-04T02:52:02Z
Duplicate of #75
๐ 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
167.5763 USDC - $167.58
fillAsk
transfer
method should be avoidedThe main concerns are with the use of deprecated methods - native transfer - and lack of checks in NFT transfer methods - ERC721.transferFrom.
Properly functioning code should never reach a failing assert statement. If it happened, it would indicate the presence of a bug in the contract. A failing assert uses all the remaining gas, which can be financially painful for a user.
Low
13 instances include:
Manual Analysis
Replace the assert statements with a require statement or a custom error
Constructors should check the value of arguments - ie make revert if it is the zero address. An incorrect address passed would mean the contracts would need to be re-deployed.
Low
Instances include:
In all contracts, _governance
is the admin address that can call key admin functions.
weth
and rewardToken
are state variables, but are never modified in the contracts, meaning an incorrect value passed to them cannot be undone.
Manual Analysis
Add non-zero checks for these function arguments.
fillAsk
When a user fills an ask order by calling fillAsk
, the ERC721.transferFrom
method is used to transfer the NFT to the receiver
. Should the receiver
be a smart contract that is unable to handle ERC721 tokens, this would result in the NFT being locked forever.
Low
Instances include:
Manual Analysis
Consider using a safeTransferFrom
method. It is a common approach to avoid using this sort of method to avoid reentrancy, but as it is not mentioned in the readme, this issue is still worth raising.
cancelOrder
can be called even for orders that have been filled. While this does not result in any loss of funds, it can be highly misleading to see an event OrderCancelled
for an order which has been filled.
Low
cancelOrder
does an internal call to validateOrder(o)
, but does not check its first return value, which indicates the order status.
Note that it means an expired order can also be cancelled
Manual Analysis
Ensure cancelOrder
only works if the order is valid
-314: (, bytes32 hashStruct, ) = validateOrder(o); + (uint256 status, bytes32 hashStruct, ) = validateOrder(o); + require(status == 3, "order is not valid")
strings and bytes are encoded with padding when using abi.encodePacked
. This can lead to hash collision when passing the result to keccak256
Low
Instances include:
hash
could potentially be the same for two different orders o
and o_
because of this issue.Manual Analysis
Use abi.encode()
instead.
Order makers can create Ask orders (type 0) with valid signatures, and frontrun fillAsk
calls by transferring the o.tokenId
to a different wallet.
This is frustrating for users as they make the assumption the order is valid, and it also makes it difficult for off-chain tooling to track 'reel' orders.
Low
Instances include:
Manual Analysis
Consider using an escrow. Another possible solution would be to add an extra step in cancelOrder
, where any user could cancel an order if ERC721.ownerOf(o.tokenId) != o.signer
status
is 3, meaning the order is still valid and has not been filled (as per these comments).Manual Analysis
The error string should be amount specified too high
transfer
method should be avoidedIn GolomTrader
, the .transfer()
method is used in payEther
to handle all ETH transfers in bid and ask functions.
The transfer()
call requires that the recipient has a payable callback, only provides 2300 gas for its operation. This means the following cases can cause the transfer to fail:
Low
Instances include:
Manual Analysis
use .call()
instead.
In GolomTrader
, There is a division before multiplication in the computation of protocolFees
. This leads to a rounding error, meaning some protocolFees
are lost.
Because the divisor is only 10,000
- The lost amount is actually very low.
Low
Let's take the example of an ERC1155 order with amount = 1,000
and o.totalAmt =2,469
.
the fee is 12,000
while it should be 12,345
, ie a 2.8% difference.
Obviously the o.totalAmt
is ridiculously low in this example - order of magnitude of 1e-15 ETH
- so the fee loss will be much smaller for reel orders.
It is however worth noting that the fee loss is inversely proportional to o.totalAmt
and proportional to tokenAmt
, so the total fee loss will be higher for ERC1155 orders where the amount of tokens is very high. No loss for ERC721 tokens.
Manual Analysis
-381: uint256 protocolfee = ((o.totalAmt * 50) / 10000) * amount; +381: uint256 protocolfee = o.totalAmt * 50 * amount / 10000;
This would only overflow if o.totalAmt * 50 * amount > 2**256 - 1
. 2**256 - 1
being the order of magnitude of 1e77
, this is very unlikely.
RewardDistributor.sol has receive()
and fallback()
functions, but does not have any ETH withdrawal method. Any ETH mistakenly sent to this contract would be locked.
Low
Instances include:
Manual Analysis
Remove these functions, or include logic in receive()
and fallback()
, so that a user that mistakenly sends ETH to the Distributor retrieves it immediately.
Some of the function comments are missing function parameters or returns
Non-Critical
7 instances include:
Manual Analysis
Add a comment for these parameters
There are portions of commented code in some files.
Non-Critical
Manual Analysis
Remove commented code
It is best practice to use constant variables rather than literal values to make the code easier to understand and maintain.
Non-Critical
26 instances include:
Manual Analysis
Define constant variables for the literal values aforementioned.
Events should use indexed fields
Non-Critical
2 instances include:
Manual Analysis
Add indexed fields to these events so that they have the maximum number of indexed fields possible.
Setters should emit an event so that Dapps can detect important changes to storage
Non-Critical
4 instances include:
Manual Analysis
Emit an event in all setters.
Most comments have a space after //
, expect for this one instance where there is no space
Non-Critical
Instances include:
Manual Analysis
Change in // deadline
It is good practice to mark functions as external
instead of public
if they are not called by the contract where they are defined.
Non-Critical
Instances include:
Manual Analysis
Declare these functions as external
instead of public
Follow common practices for variables naming. This also helps with readability:
Low
Instances include:
Manual Analysis
Change to delegatedNft
and minVotingPowerRequired
Some require statements are missing error strings, which makes it more difficult to debug when the function reverts.
Non-critical
29 instances:
Manual Analysis
Add error strings to all require statements.
For readability, it is best to use scientific notation (e.g 10e5
) rather than decimal literals(100000
) or exponentiation(10**5
)
Non-Critical
10 instances include:
Manual Analysis
Replace the numbers aforementioned with their scientific notation
Same conditions should have the same revert strings
Non-Critical
2 instances include:
Manual Analysis
Ensure the same revert string is used
RewardDistributor start time is set to a time which is anterior to the audit end date.startTime = 1659211200; => 30/7/22 and the contest ends 1/8/22
Non-Critical
Manual Analysis
Ensure startTime
corresponds to the date you are deploying the contracts
There are typos in the contract
Non-Critical
26 instances include:
facilitating
until
- till is not an abbreviation of until.if deadline has passed
Manual Analysis
Correct the typos
๐ 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
94.6571 USDC - $94.66
Several optimizations allow to save significant amounts of gas upon deployment of contracts. The report separates these high savings issues from lower gas saving issues, which are more informational. The most interesting saving is in
RewardDistributor.sol
, where setting variables that are not modified asimmutable
allow to save a lot of gas onfillAsk
,fillBid
andfillCriteriaBid
, which are functions invoked by users inGolomTrader
. This is the first issue detailed in this report. It is also a very simple change for the team to make Changingrequire
statements into custom errors is also worth considering, as there is many instances (75) across the contracts, and each change would yield high gas savings upon deployments.
If a variable is set in the constructor and never modified afterwards, marking it as immutable
can save a storage slot.
4 instances:
Manual Analysis
Mark these variables as immutable
.
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2003774 |
This saves 10,068
gas upon deployment for GolomTrader.sol
Things are a bit more complex here. If we only make startTime
immutable:
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | fillAsk | 241397 | ||
------------------ | ------------------- | ----- | ----- | -------- |
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | fillAsk | 239294 | ||
------------------ | ------------------- | ----- | ----- | -------- |
RewardDistributor | Deployment | 2369708 |
This saves 9,829
gas upon deployment for RewardDistributor.sol
. But more importantly, this saves 2,103
gas on fillAsk
. This is because fillAsk
does an external call to addFee
, where startTime
is fetched.
Now, if we also make rewardToken
and weth
immutable:
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | fillAsk | 241397 | ||
------------------ | ------------------- | ----- | ----- | -------- |
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | fillAsk | 237164 | ||
------------------ | ------------------- | ----- | ----- | -------- |
RewardDistributor | Deployment | 2390342 |
It does cost 10,805
more upon deployment, but fillAsk
calls are 4,237
gas cheaper than the original contract.
As fillBid
and fillCriteriaBid
also make an external call to RewardDistributor.addFee
, they also benefit from these savings.
State variables are expensive as they take storage slots. If they are unused, they should be deleted
Manual Analysis
Remove this variable.
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2003575 |
Removing it saves 10,267
gas upon deployment.
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,but it alleviates the compiler from the abi.decode()
step that copies each index of the calldata to the memory index, each iteration costing 60
gas.
Instances:
scope: _verifyProof()
scope: addFee
scope: traderClaim
scope: exchangeClaim
scope: multiStakerClaim
Manual Analysis
Replace memory
with calldata
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 1977363 |
This saves 36,479
gas upon deployment for GolomTrader.sol
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2349436 |
This saves 30,101
gas upon deployment for RewardDistributor.sol
Booleans are more expensive than uint256: each write operation emits an extra SLOAD
to first read the slot's contents, replace the bits taken up by the boolean, and then write back.
Compared to using uint256
, this costs an extra 100
gas, even one Gsset 20000
gas when changing from 'false' to 'true' - if this is not the first time setting it to true
.
Instances:
Manual Analysis
Use uint256(2)
and uint256(1)
instead of true
and false
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomToken | mintAirdrop | 142282 | ||
GolomToken | mintGenesisReward | 142260 | ||
------------ | ------------------- | ----- | ----- | -------- |
GolomToken | Deployment | 1998674 |
Total cost including deployment: 2 283 216 gas
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomToken | mintAirdrop | 125032 | ||
GolomToken | mintGenesisReward | 125010 | ||
------------ | ------------------- | ----- | ----- | -------- |
GolomToken | Deployment | 2029688 |
Total cost including deployment: 2 279 730 gas
This saves 3,486
gas in total for GolomToken.sol
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 1997028 |
This saves 16,814
gas upon deployment for GolomTrader.sol
Gas can be saved by factoring a function.
(CONST * (a - b)) / a is equivalent to CONST - CONST * b /a
Instances:
Manual Analysis
- uint256 tokenToEmit = (dailyEmission * (rewardToken.totalSupply() - rewardToken.balanceOf(address(ve)))) / - rewardToken.totalSupply(); + uint256 tokenToEmit = dailyEmission - dailyEmission * rewardToken.balanceOf(address(ve))/ + rewardToken.totalSupply() ;
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2355564 |
This saves 23,973
gas upon deployment for RewardDistributor.sol
.
This may however cause imprecision due to dailyEmission * rewardToken.balanceOf(address(ve))/ rewardToken.totalSupply()
when rewardToken.balanceOf(address(ve))
is very low.
Marking constants as private
save gas upon deployment, as the compiler does not have to create getter functions for these variables. It is worth noting that a private
variable can still be read using either the verified contract source code or the bytecode.
Instances:
Manual Analysis
Make the constants private
instead of public
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2000501 |
This saves 13,341
gas upon deployment for GolomTrader.sol
Where it does not affect readability, using assembly allows to save gas not only on deployment, but also on function calls. This is the case for instance for simple admin setters.
4 instances:
Use sstore
to write the state variables new value
- pendingMinter = _minter; + assembly { + sstore(pendingMinter.slot, _minter) + }
Applying this to all instances mentioned above:
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomToken | Deployment | 1998674 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomToken | Deployment | 1993915 |
This saves 4,759
gas upon deployment for GolomToken.sol
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2009095 |
This saves 4,747
gas upon deployment for GolomTrader.sol
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2368466 |
This saves 11,071
gas upon deployment for RewardDistributor.sol
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
In particular, in for
loops, when using the length of a storage array as the condition being checked after each loop, caching the array length can yield significant gas savings if the array length is high
Instances:
scope: fillAsk()
distributor
is read twice:
payEther(((o.totalAmt * 50) / 10000) * amount, address(distributor))
distributor.addFee([o.signer, o.exchange.paymentAddress], ((o.totalAmt * 50) / 10000) * amount)
scope: _settleBalances()
WETH
is read twice:
WETH.transferFrom(o.signer, address(this), o.totalAmt * amount)
distributor
is read twice:
distributor.addFee([msg.sender, o.exchange.paymentAddress], protocolfee)
scope: addFee()
rewardToken
is read 7 times:
epoch
is read 3 times before it is written:
epoch
is read 12 times after it is written:
rewardTrader[epoch] = ((tokenToEmit - stakerReward) * 67) / 100
rewardExchange[epoch] = ((tokenToEmit - stakerReward) * 33) / 100
emit NewEpoch(epoch, tokenToEmit, stakerReward, previousEpochFee)
feesTrader[addr[0]][epoch] = feesTrader[addr[0]][epoch] + fee
feesExchange[addr[1]][epoch] = feesExchange[addr[1]][epoch] + fee
ve
is read twice:
WETH
is read twice:
scope: multiStakerClaim
ve
is read 5 or 7 times, depending on this condition:
require(tokenowner == ve.ownerOf(tokenids[tindex]), 'Can only claim for a single Address together')
(rewardStaker[epochs[index]] * ve.balanceOfAtNFT(tokenids[tindex], epochBeginTime[epochs[index]]))
epochTotalFee[epochs[index]] * ve.balanceOfAtNFT(tokenids[tindex], epochBeginTime[epochs[index]]))
You should save the return of (ve.balanceOfAtNFT(tokenids[tindex], epochBeginTime[epochs[index]]))
and ve.totalSupplyAt(epochBeginTime[epochs[index]])
to save two external calls.
epochBeginTime[1]
is read twice:
scope: stakerRewards
ve
is read 3 or 5 times, depending on this condition:
You should save the return of ve.balanceOfAtNFT(tokenid, epochBeginTime[index]))
and ve.totalSupplyAt(epochBeginTime[index])
to save two external calls.
epochBeginTime[1]
is read twice:
scope addVoteEscrow
ve
is read twice:
scope: approve
owner
instead of idToOwner[_tokenId]
:scope: _create_lock
948: ++tokenId; 949: uint256 _tokenId = tokenId
can be refactored in
948: uint256 _tokenId = ++tokenId
Manual Analysis
cache these storage variables using local variables.
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
75 instances:
Manual Analysis
Replace require and revert statements with custom errors.
For instance, in GolomTrader.sol
:
Replace
-222: require(o.orderType == 0, 'invalid orderType') + if (o.orderType != 0) { + revert InvalidOrderType(); + }
and define the custom error in the contract
+error InvalidOrderType();
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2006275 |
This one require statement changed into a custom error saves 7,567
gas upon deployment for GolomTrader.sol
Revert strings cost more gas to deploy if the string is larger than 32 bytes.
Revert strings exceeding 32 bytes include 10 instances:
Manual Analysis
Write the error strings so that they do not exceed 32 bytes. For further gas savings, consider also using custom errors.
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomToken | Deployment | 1998674 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomToken | Deployment | 1991155 |
This saves 7,519
gas upon deployment for GolomToken.sol
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2375793 |
This saves 3,744
gas upon deployment for RewardDistributor.sol
Combining mappings of address
into a single mapping of address
to a struct
can save a Gssset
(20000 gas) operation per mapping combined.
This also makes it cheaper for functions reading and writing several of these mappings by saving a Gkeccak256
operation- 30 gas.
Instances:
Manual Analysis
Combine the address
mappings aforementionned in a single address
=> struct
mapping
example:
+58: struct Fees { +59: mapping(uint256 => uint256) trader; +60: mapping(uint256 => uint256) exchange; +61: } +62: mapping(address => Fees) public fees; -58: mapping(address => mapping(uint256 => uint256)) public feesTrader; // fees accumulated by address of trader per epoch -60: mapping(address => mapping(uint256 => uint256)) public feesExchange; // fees accumulated by exchange of trader per epoch
It wastes gas to read an array's length in every iteration of a for
loop, even if it is a memory or calldata array: 3
gas per read.
Instances:
Manual Analysis
Caching the length in a variable before the for
loop
If the string can fit into 32 bytes, then bytes32
is cheaper than string
. string
is a dynamically sized-type, which has current limitations in Solidity compared to a statically sized variable.
Instances:
Manual Analysis
Replace string constant
with bytes(1..32) constant
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters.
4 nstances:
Manual Analysis
Hardcode these variables with their initial value instead of writing it during contract deployment with constructor parameters.
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 for state variables - not for stack variables.
2 instances:
Manual Analysis
Remove explicit initialization for default values in state variables.
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2379537 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RewardDistributor | Deployment | 2377263 |
This saves 2,274
gas upon deployment for RewardDistributor.sol
Empty blocks in receive
or fallback
functions should emit an event, or revert. If not, they can simply be removed to save gas upon deployment
5 instances:
Manual Analysis
Emit an event in these blocks, or remove them altogether.
block.timestamp
and block.number
are added to event information by default, explicitly adding them is a waste of gas.
1 instance:
Manual Analysis
Remove the event field uint256 ts
from Withdraw
When we define internal functions to perform computation:
When it does not affect readability, it is recommended to inline internal
functions that are called only once.
Instances:
Manual Analysis
Inline these functions where they are called. Another method is to use the viaIR
compiler optimization flag, which helps with inline optimizations.
X += Y costs 22
more gas than X = X + Y. This can mean a lot of gas wasted in a function call when the computation is repeated n
times (loops)
18 instances include:
Manual Analysis
use X = X + Y
instead of X += Y
(same with -
)
When a require
statement is used multiple times, it is cheaper in deployment costs to use a modifier instead.
18 instances include:
require(epochs[index] < epoch, 'cant claim for future epochs')
require(traderEnableDate <= block.timestamp, 'RewardDistributor: time not over yet')
require(voteEscrowEnableDate <= block.timestamp, 'RewardDistributor: time not over yet')
require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed')
require(ownerOf(tokenId) == msg.sender, 'VEDelegation: Not allowed')
require(blockNumber < block.number, 'VEDelegation: not yet determined')
require(blockNumber < block.number, 'VEDelegation: not yet determined')
require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached')
require(attachments[_from] == 0 && !voted[_from], 'attached')
require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached')
require(unlock_time > block.timestamp, 'Can only lock until time in the future')
require(unlock_time <= block.timestamp + MAXTIME, 'Voting lock can be 4 years max')
require(_locked.end > block.timestamp, 'Cannot add to expired lock. Withdraw')
require(_locked.end > block.timestamp, 'Cannot add to expired lock. Withdraw')
Manual Analysis
Use modifiers for these repeated statements
Prefix increments are cheaper than postfix increments - 6
gas. This can mean interesting savings in for
loops.
14 instances:
Manual Analysis
change variable++
to ++variable
, and variable += 1
to ++variable
.
Require statements including conditions with the &&
operator can be broken down in multiple require statements to save gas upon function calls. This does cost more upon deployment, meaning there is a trade-off to be decided by the team.
4 instances:
require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached')
require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached')
require(attachments[_from] == 0 && !voted[_from], 'attached')
require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached')
Manual Analysis
Break down the require statements in multiple statements
- require(attachments[_tokenId] == 0 && !voted[_tokenId], 'attached') + require(attachments[_tokenId] == 0) + require(!voted[_tokenId])
A division by 2 can be calculated by shifting one to the right. 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.
3 instances:
Manual Analysis
Replace / 2
with >> 1
Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage. By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.
address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).
As bool and uint8 type variables are of size 1 byte, there's two slots in a struct that can get saved by moving them closer to an address
Instances :
55: bool isERC721; // standard of the collection , if 721 then true , if 1155 then false 56: uint256 tokenAmt; // token amt useful if standard is 1155 if >1 means whole order can be filled tokenAmt times 57: uint256 refererrAmt; // amt to pay to the address that helps in filling your order 58: bytes32 root; // A merkle root derived from each valid tokenId รขโฌโ set to 0 to indicate a collection-level or tokenId-specific order. 59: address reservedAddress; // if not address(0) , only this address can fill the order 60: uint256 nonce; // nonce of order usefull for cancelling in bulk 61: uint256 deadline; // timestamp till order is valid epoch timestamp in secs 62: uint8 v; 63: bytes32 r; 64: bytes32 s;
Manual Analysis
Rearrange the struct:
-55: bool isERC721; // standard of the collection , if 721 then true , if 1155 then false 56: uint256 tokenAmt; // token amt useful if standard is 1155 if >1 means whole order can be filled tokenAmt times 57: uint256 refererrAmt; // amt to pay to the address that helps in filling your order 58: bytes32 root; // A merkle root derived from each valid tokenId รขโฌโ set to 0 to indicate a collection-level or tokenId-specific order. 59: address reservedAddress; // if not address(0) , only this address can fill the order + bool isERC721; // standard of the collection , if 721 then true , if 1155 then false + uint8 v; 60: uint256 nonce; // nonce of order usefull for cancelling in bulk 61: uint256 deadline; // timestamp till order is valid epoch timestamp in secs -62: uint8 v; 63: bytes32 r; 64: bytes32 s;
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013842 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
GolomTrader | Deployment | 2013782 |
This saves 60
gas upon deployment for GolomTrader.sol
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked
block will save gas.
This is also true for loop increments, which cannot overflow because of a block gas limit.
Instances:
Manual Analysis
Place the arithmetic operations in an unchecked
block
Stack variables cost gas, remove then when they are redundant.
323: function incrementNonce() external nonReentrant { 324: uint256 newNonce = ++nonces[msg.sender]; 325: emit NonceIncremented(msg.sender, newNonce); 326: }
Manual Analysis
+325: emit NonceIncremented(msg.sender, ++nonces[msg.sender]); -324: uint256 newNonce = ++nonces[msg.sender]; -325: emit NonceIncremented(msg.sender, newNonce);