Platform: Code4rena
Start Date: 12/07/2022
Pot Size: $75,000 USDC
Total HM: 16
Participants: 100
Period: 7 days
Judge: LSDan
Total Solo HM: 7
Id: 145
League: ETH
Rank: 22/100
Findings: 2
Award: $412.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 8olidity, Aussie_Battlers, Bnke0x0, Ch_301, Critical, Deivitto, Dravee, ElKu, Funen, GimelSec, JC, JohnSmith, Lambda, MiloTruck, PwnedNoMore, ReyAdmirado, Rohan16, Rolezn, Ruhum, RustyRabbit, Sm4rty, TomJ, Waze, _Adam, __141345__, alan724, asutorufos, benbaessler, berndartmueller, bin2chen, brgltd, bulej93, c3phas, cRat1st0s, cryptonue, cryptphi, csanuragjain, delfin454000, dxdv, exd0tpy, fatherOfBlocks, gogo, hake, hyh, joestakey, kyteg, lcfr_eth, minhtrng, p_crypt0, pashov, pedr02b2, philogy, rajatbeladiya, rbserver, rishabh, robee, rokinot, sach1r0, sashik_eth, seyni, simon135, svskaushik, zuhaibmohd, zzzitron
177.9856 USDC - $177.99
LOW
transfer
methodNON - CRITICAL
transfer
methodIn ETHRegistrarController
, the .transfer()
method is used to refund the caller if they sent more ETH than the price required.
The transfer()
call requires that the recipient has a payable callback, only providing 2300 gas for its operation. This means the following cases can cause the transfer to fail if register
or renew
are called by a smart contract:
Low
Instances include:
payable(msg.sender).transfer(msg.value - (price.base + price.premium))
payable(msg.sender).transfer(msg.value - price.base)
payable(owner()).transfer(address(this).balance)
Manual Analysis
Use call
instead of transfer
.
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:
bytes32 nodehash = keccak256(abi.encodePacked(ETH_NODE, label))
bytes32 reverseNode = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, labelHash))
return keccak256(abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr)))
return keccak256(abi.encodePacked(node, labelhash))
Manual Analysis
Use abi.encode()
instead.
constructors should check the address written in an immutable address variable is not the zero address. This is also valid for implementation addresses.
Low
Instances include:
base = _base
prices = _prices
reverseRegistrar = _reverseRegistrar
nameWrapper = _nameWrapper
ens = _ens
registrar = _registrar
Manual Analysis
Add a zero address check for the immutable variables aforementioned.
Setters and initializers should check the input value - ie make revert if it is the zero address
Low
Instances include:
function setAlgorithm
function setDigest
Manual Analysis
Add non-zero address checks.
Visibility (public / external) is not needed for constructors anymore since Solidity 0.7.0, see here
Non-Critical
Instances include:
Manual Analysis
Remove the public
modifier.
Events should be emitted after all external calls, to prevent the risk of a function reverting while the event had been emitted.
Non-Critical
Instances include:
ens
Manual Analysis
emit these events at the end of the functions they are in.
Events should use indexed fields, to make them easier to filter in logs. This can be interesting for fields such as address
indicating the new owner of a token, or filter through which controllers have been made active.
Non-Critical
Instances include:
event AlgorithmUpdated(uint8 id, address addr)
event DigestUpdated(uint8 id, address addr)
event NameRegistered(string name,bytes32 indexed label,address indexed owner,uint256 baseCost,uint256 premium,uint256 expires)
event NameRenewed(string name,bytes32 indexed label,uint256 cost,uint256 expires)
event NameWrapped(bytes32 indexed node,bytes name,address owner,uint32 fuses,uint64 expiry)
event NameUnwrapped(bytes32 indexed node, address owner)
event FusesSet(bytes32 indexed node, uint32 fuses, uint64 expiry)
event ControllerChanged(address indexed controller, bool active)
event Transfer(bytes32 indexed node, address owner)
event NewResolver(bytes32 indexed node, address resolver)
event NewTTL(bytes32 indexed node, uint64 ttl)
event ApprovalForAll(address indexed owner,address indexed operator,bool approved)
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
Instances include:
function setMetadataService
function setUpgradeContract
Manual Analysis
emit an event in all setters
Some functions are missing Natspec comments
Non-Critical
Instances include:
function readSignedSet
function rrs
function readDNSKEY
function readDS
function compareNames
function progress
All the functions are missing comments.
function setDefaultResolver
function ownsContract
Manual Analysis
Add comments to these functions
Functions should be ordered following the Solidity conventions. This means ordering functions based on their visibility.
Non-Critical
Several contracts have external functions defined after public functions:
DNSSECImpl.sol
ETHRegistrarController.sol
Some have internal functions defined after private functions:
And this contract has both:
Manual Analysis
Order the functions as per the Solidity conventions: external, public, internal, private.
All interface functions are implicitly virtual.
Non-Critical
function verify(bytes calldata key, bytes calldata data, bytes calldata signature) external virtual
function verify(bytes calldata data, bytes calldata hash) external virtual
Compiler warnings
Remove the virtual
modifier from these functions.
In NameWrapper.sol
, there is local variable shadowing: several functions declare a local variable owner
, which is the name of a state variable - inherited from Ownable
. This will not lead to any error but can be confusing.
Non-critical
Instances include:
Manual Analysis
Add an underscore (_owner
) to avoid shadowing.
contracts should be compiled using a fixed compiler version. Locking the pragma helps ensure that contracts do not accidentally get deployed using a different compiler version with which they have been tested the most
Non-Critical
None of the contracts in scope have fixed pragmas.
Manual Analysis
Used a fixed compiler version
There are open TODOs in the code. Code architecture, incentives, and error handling/reporting questions/issues should be resolved before deployment.
Non-Critical
Instances include:
// TODO: Check key isn't expired, unless updating key itself
Manual Analysis
Remove the TODOs
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:
function setAlgorithm()
function setDigest()
function available()
function commit()
function register()
function withdraw()
function setDefaultResolver()
function claimWithResolver()
function node()
function setUpgradeContract()
function setFuses()
function upgradeETH2LD()
function upgrade()
function setChildFuses()
function setSubnodeOwner()
function setSubnodeRecord()
Manual Analysis
Declare these functions as external
instead of public
Require statements should have descriptive error strings, to make it easier to debug when a function reverts.
Non-Critical
Instances include:
Manual Analysis
Add descriptive revert strings
Several contracts are missing SPDX identifiers which correctly license the contract for open source development
Non-Critical
Instances include:
Compiler warnings
The oracle contract DNSSECImpl
uses an implementation of Owned
to determine its owner
. Consider using a two-steps ownership transfer method in Owned
, where the new owner needs to approve the ownership. This mitigates the risk of transferring ownership to a random address.
Non-Critical
Instances include:
Manual Analysis
#0 - jefflau
2022-07-26T08:18:10Z
Events emitted early Events should be emitted after all external calls, to prevent the risk of a function reverting while the event had been emitted.
This is not how the EVM works, the revert will also revert the event.
I've marked this issue as disputed for this specific bug
🌟 Selected for report: 0xKitsune
Also found by: 0x040, 0x1f8b, 0x29A, 0xNazgul, 0xNineDec, 0xsam, 8olidity, Aussie_Battlers, Aymen0909, Bnke0x0, CRYP70, Ch_301, Chom, Deivitto, Dravee, ElKu, Fitraldys, Funen, GimelSec, IllIllI, JC, JohnSmith, Lambda, MiloTruck, Noah3o6, RedOneN, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Tomio, Waze, _Adam, __141345__, ajtra, ak1, arcoun, asutorufos, benbaessler, brgltd, bulej93, c3phas, cRat1st0s, cryptonue, delfin454000, durianSausage, fatherOfBlocks, gogo, hake, hyh, joestakey, karanctf, kyteg, lcfr_eth, lucacez, m_Rassska, rajatbeladiya, rbserver, robee, rokinot, sach1r0, sahar, samruna, sashik_eth, seyni, simon135, zuhaibmohd
234.0746 USDC - $234.07
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.
5 instances:
Manual Analysis
Caching the length in a variable before the for
loop
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
.
2 instances:
Manual Analysis
Replace bool
with uint256
.
Use uint256(1)
and uint256(2)
instead of true
/false
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
2 instances:
scope: _consumeCommitment
:
commitments- [commitment]
is read twice, it can be cached with a local variable
commitments- [commitment] + minCommitmentAge <= block.timestamp
commitments- [commitment] + maxCommitmentAge > block.timestamp
scope: setUpgradeContract
:
upgradeContract
is read 3 times, while it could be cached with the function argument _upgradeAddress
:
Manual Analysis
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.
2 instances:
Manual Analysis
Make the constants private
instead of public
·-----------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ······················································|···························|···············|······························ | Deployments · · % of limit · │ ······················································|·············|·············|···············|···············|·············· | ETHRegistrarController · - · - · 2010419 · 6.4 % · - │ ·-----------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
·-----------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ······················································|···························|···············|······························ | Deployments · · % of limit · │ ······················································|·············|·············|···············|···············|·············· | ETHRegistrarController · - · - · 1974221 · 6.4 % · - │ ·-----------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
This saves 36,198
gas upon deployment.
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
41 instances:
Manual Analysis
Replace require and revert statements with custom errors.
For instance, in ETHRegistrarController.sol
:
-require(resolver != address(0),"ETHRegistrarController: resolver is required when data is supplied") +if (resolver == address(0)) { + revert ResolverRequired(); +}
and define the custom error in the contract
+error ResolverRequired();
A function with access control marked as payable will lbe cheaper for legitimate callers: the compiler removes checks for msg.value
, saving approximately 20
gas per function call.
6 instances:
Manual Analysis
Mark these functions as payable
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.
3 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)
9 instances include:
Manual Analysis
use X = X + Y
instead of X += Y
(same with -
). If Y ==1
, use ++Y
or --Y
.
When a require
statement is used multiple times, it is cheaper in deployment costs to use a modifier instead.
4 instances include:
require(to != address(0), "ERC1155: transfer to the zero address")
require(to != address(0), "ERC1155: transfer to the zero address")
require(from == msg.sender || isApprovedForAll(from, msg.sender),"ERC1155: caller is not owner nor approved")
require(from == msg.sender || isApprovedForAll(from, msg.sender),"ERC1155: caller is not owner nor approved")
require(amount == 1 && oldOwner == from,"ERC1155: insufficient balance for transfer")
require(amount == 1 && oldOwner == from,"ERC1155: insufficient balance for transfer")
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.
5 instances:
Manual Analysis
change variable++
to ++variable
.
Revert strings cost more gas to deploy if the string is larger than 32 bytes.
22 instances:
Manual Analysis
Write the error strings so that they do not exceed 32 bytes. For further gas savings, consider also using - custom errors.
For instance, changing all strings in ETHRegistrarController.sol
to strings shorter than 32 bytes:
·-----------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ······················································|···························|···············|······························ | Deployments · · % of limit · │ ······················································|·············|·············|···············|···············|·············· | ETHRegistrarController · - · - · 2010419 · 6.4 % · - │ ·-----------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
·-----------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ······················································|···························|···············|······························ | Deployments · · % of limit · │ ······················································|·············|·············|···············|···············|·············· | ETHRegistrarController · - · - · 1932761 · 6.4 % · - │ ·-----------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
This saves 77,658
gas upon deployment, an average of 11,094
gas saved per revert string.
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
9 instances:
Manual Analysis
Place the arithmetic operations in an unchecked
block.
For instance, doing it for the 3 instances mentioned in ETHRegistrarController
:
·-----------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ······················································|···························|···············|······························ | Methods │ ································|·····················|·············|·············|···············|···············|·············· | Contract · Method · Min · Max · Avg · # calls · eur (avg) │ ································|·····················|·············|·············|···············|···············|·············· | ETHRegistrarController · register · 233372 · 377120 · 293714 · 15 · - │ ································|·····················|·············|·············|···············|···············|·············· | Deployments · · % of limit · │ ······················································|·············|·············|···············|···············|·············· | ETHRegistrarController · - · - · 2010419 · 6.7 % · - │ ·-----------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
·-----------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ······················································|···························|···············|······························ | Methods │ ································|·····················|·············|·············|···············|···············|·············· | Contract · Method · Min · Max · Avg · # calls · eur (avg) │ ································|·····················|·············|·············|···············|···············|·············· | ETHRegistrarController · register · 233244 · 376921 · 293558 · 15 · - │ ································|·····················|·············|·············|···············|···············|·············· | Deployments · · % of limit · │ ······················································|·············|·············|···············|···············|·············· | ETHRegistrarController · - · - · 1997901 · 6.7 % · - │ ·-----------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
It saves 12,518
upon deployment, and an average of 156
gas per register
call - here for data.length == 15
, so an average of 10
gas per byte of data.
700
gasAll the contracts in scope have the pragma set to ^0.8.4
Manual Analysis
Upgrade these contracts compiler versions.
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.
2 instances:
Use assembly to write the new value in storage. For instance in NameWrapper.sol
:
- metadataService = _newMetadataService; + assembly { + sstore(metadataService.slot, _newMetadataService) + }
·---------------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ··························································|···························|···············|······························ | Methods │ ································|·························|·············|·············|···············|···············|·············· | Contract · Method · Min · Max · Avg · # calls · eur (avg) │ ································|·························|·············|·············|···············|···············|·············· | NameWrapper · setMetadataService · - · - · 29013 · 1 · - │ ································|·························|·············|·············|···············|···············|·············· | Deployments · · % of limit · │ ··························································|·············|·············|···············|···············|·············· | NameWrapper · - · - · 5250093 · 17.5 % · - │ ·---------------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
·---------------------------------------------------------|---------------------------|---------------|-----------------------------· | Solc version: 0.8.13 · Optimizer enabled: true · Runs: 10000 · Block limit: 30000000 gas │ ··························································|···························|···············|······························ | Methods │ ································|·························|·············|·············|···············|···············|·············· | Contract · Method · Min · Max · Avg · # calls · eur (avg) │ ································|·························|·············|·············|···············|···············|·············· | NameWrapper · setMetadataService · - · - · 28962 · 1 · - │ ································|·························|·············|·············|···············|···············|·············· | Deployments · · % of limit · │ ··························································|·············|·············|···············|···············|·············· | NameWrapper · - · - · 5238900 · 17.5 % · - │ ·---------------------------------------------------------|-------------|-------------|---------------|---------------|-------------·
It saves 11,193
gas upon deployment, and an average of 51
gas per function call.
#0 - jefflau
2022-08-01T09:45:42Z
High quality submission