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: 10/100
Findings: 4
Award: $1,263.53
π Selected for report: 0
π Solo Findings: 0
π Selected for report: panprog
Also found by: Aussie_Battlers, brgltd, cryptphi, peritoflores, wastewa
Function NameWrapper.setSubnodeOwner
can be used to transfer ownership of a sub-domain to a new owner and, at the same time, burn fuses. A possible use-case could be that a domain owner wants to transfer ownership of the sub-domain but burn fuses in order to restrict what the sub-domain owner can do.
Unfortunately, the sub-domain owner -- through a malicious contract -- can use the onERC1155Received
hook to call back into the NameWrapper
contract to perform actions that the fuses would prohibit. This is made possible because of the way _transferAndBurnFuses
is written. A _transfer
, which transfers control to the malicious smart contract, is done before setFuses
is called.
The burning of fuses can prohibit:
Oh all the things a domain holder might want to prohibit in sub-domains probably the most likely would be the setting of a resolver. The domain holder could, quite legitimately, want to prohibit a sub-domain holder to set a resolver that would cause a web browser to a dangerous IP address for the purpose of scamming.
Another thing a domain owner might want to prohibit is the creating of further sub-domains. Unfortunately, using this exploit the malicious sub-domain owner could create as many sub-sub-domains as they wanted.
In all cases, the situation is made much worse by the fact that once setSubnodeOwner
has finished executing the domain owner can no longer perform actions on the sub-domain! This is because PARENT_CANNOT_CONTROL
must have been burned. A parent cannot burn sub-domain fuses unless both PARENT_CANNOT_CONTROL
and CANNOT_UNWRAP
are also burned. See lines 954-962.
This exploit works equally well with NameWrapper.setSubnodeRecord
because it also calls _transferAndBurnFuses
.
newOwner
set to themselves in order to create a sub-domain which they still own.setSubnodeOwner
again but with a newOwner
set to the new sub-domain owner. They also use a value for fuses
that locks down its functionality somehow. e.g. PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_SET_RESOLVER
._transferAndBurnFuses
is called which eventually executes line 820.onERC1155Received
hook to call back into NameWrapper
to perform some action. e.g. setSubnodeOwner
so it can create a new sub-domain or setRecord
to set the resolver and the TTL. These calls succeed since
A proof of concept of the a) resolver setting and b) sub-domain creating scenarios can be executed by applying the diff in this gist and then running
$ npx hardhat test test/wrapper/BugsSetSubnodeOwner.js
Manual inspection + hardhat
The root cause of this issue is the lack of re-entrancy protection on many of the NameWrapper
functions. Although costly in terms of gas, re-entrancy protection should be added perhaps using Open Zeppelin's ReentrancyGuard contract.
#0 - Arachnid
2022-07-27T03:29:15Z
Duplicate of #84.
π Selected for report: rajatbeladiya
Also found by: 0x29A, 0xNineDec, Amithuddar, Aussie_Battlers, Ch_301, Dravee, GimelSec, IllIllI, Jujic, Limbooo, RedOneN, Ruhum, TomJ, _Adam, __141345__, alan724, asutorufos, berndartmueller, c3phas, cccz, cryptphi, durianSausage, fatherOfBlocks, hake, hyh, pashov, scaraven, zzzitron
5.45 USDC - $5.45
https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L182-L185 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L204 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L210-L212
The use of the deprecated transfer() function for an address will inevitably make the transaction fail when:
The receiver smart contract does not implement a payable function.
The receiver smart contract does implement a payable fallback which uses more than 2300 gas unit.
The receiver 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.
Use call()
instead of transfer()
to send ETH. And return value must be checked to see if the call is successful. Sending ETH with the transfer is no longer recommended.
#0 - jefflau
2022-07-22T09:50:42Z
Duplicate of #133
π 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
79.4817 USDC - $79.48
PARENT_CANNOT_REPLACE
--> PARENT_CANNOT_CONTROL
Avoid floating pragmas for non-library contracts.
While floating pragmas make sense for libraries to allow them to be included with multiple different versions of applications, it may be a security risk for application implementations.
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.
It is recommended to pin to a concrete compiler version.
Checking addresses against zero-address during initialization or during setting is a security best-practice. However, such checks are missing in address variable initializations/changes in many places.
Impact: Allowing zero-addresses will lead to contract reverts and force redeployments if there are no setters for such address variables.
Recommend considering implementing a two step process where the owner or admin nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.
INameWrapper.sol
defines 7 fuses but there is space for up to 32 in the uint32
type. Fuses appear to be one-way in the sense that you can burn them and they only get reset when the expiry runs out.
setFuses
allows arbitrary fuses to be burned because it accepts an arbitrary uint32 fuses
parameter.
What happens when/if new fuses are added in the future? Would the old fuses get migrated? If so, could the fact that some have been set cause problems?
Add a check to see if the fuses being burned are valid fuses in setFuses
and revert otherwise.
block.timestamp
Block timestamps have historically been used for a variety of applications, such as entropy for random numbers (see the Entropy Illusion for further details), locking funds for periods of time, and various state-changing conditional statements that are time-dependent. Miners have the ability to adjust timestamps slightly, which can prove to be dangerous if block timestamps are used incorrectly in smart contracts.
Block timestamps should not be used for entropy or generating random numbersβi.e., they should not be the deciding factor (either directly or through some derivation) for winning a game or changing an important state.
Time-sensitive logic is sometimes required; e.g., for unlocking contracts (time-locking), completing an ICO after a few weeks, or enforcing expiry dates. It is sometimes recommended to use block.number and an average block time to estimate times; with a 10 second block time, 1 week equates to approximately, 60480 blocks. Thus, specifying a block number at which to change a contract state can be more secure, as miners are unable to easily manipulate the block number.
A user can call commit()
a large number of times to fill up the commitments
mapping with expired or invalid data.
Commitments are only removed from the mapping when a valid commit is consumed
ETHRegistrarController.sol:L244
Recommend adding a function to periodically clear expired commitments.
ETHRegistrarController.sol:L122
bytes.concat
instead of abi.encodePacked
Function bytes.concat
can be used in place of abi.encodePacked
in many locations. e.g. NameWrapper.sol:733-753.
There are known issues with abi.encodePacked
. Even those abi.encodePacked
is safe as it is used in the ENS code-base there have been proposals to remove it from Solidity.
The solution uses:
"@openzeppelin/contracts": "^4.1.0",
there has been multiple security patches released for openzeppelin/contracts
since this version
Openzeppelin Security Advisories
Upgrade @openzeppelin/contracts
to 4.4.2 or higher
BaseRegistrarImplementation
not quite rightSince Solidity 0.8 arithmetic overflow is checked and leads to a revert unless you use an unchecked
block.
Line 122 and Line 141 both check whatever the new expiries[id]
will be set to it will not overflow when GRACE_PERIOD
is added to it.
However, in the case that you are trying to protect against, the expression inside the require will overflow itself before evaluating to true or false.
The function will revert, which is the intended effect, but for the wrong reason. Consider rewriting the logic as follows (just for clarity).
For _register
require(type(uint256.max) - (block.timestamp + duration) >= GRACE_PERIOD);
and for renew
require(type(uint256).max - (expiries[id] + duration) >= GRACE_PERIOD);
You might even want to add a user-friendly reason as to why the require
failed.
#0 - jefflau
2022-08-01T09:47:30Z
Non-critical: [N-03] Overflow checks in BaseRegistrarImplementation not quite right
BaseRegistrarImplementation is out of scope and was deployed before 0.8
π 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
39.8689 USDC - $39.87
Uninitialized variables are assigned with the types default value.
Explicitly initializing a variable with it's default value costs unnecessary gas.
Suggest not initializing the for loop counter to 0.
An arrayβs length should be cached to save gas in for-loops
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.
Suggest storing the arrayβs length in a variable before the for-loop, and use it instead:
++i costs less gas compared to i++
++i costs less gas compared to i++ for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration)
Suggest using ++i instead of i++ to increment the value of an uint variable.
Increments can be unchecked
In Solidity 0.8+, thereβs a default overflow check on unsigned integers. Itβs possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
taking all of the above, the recommended format for gas savings is
for (uint256 i; i < numIterations;) { // ... unchecked { ++i; } }
Use calldata instead of memory for function parameters saves gas if the function argument is only read.
Uninitialized variables are assigned with the types default value.
Explicitly initializing a variable with it's default value costs unnecesary gas.
require statements can be placed earlier to reduce gas usage on revert ie move require to the top of the function if possible