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: 27/100
Findings: 3
Award: $250.36
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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#L183-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#L211
Using Solidity's transfer
function has some notable shortcomings when the caller is a smart contract, which can render registers via ETH
impossible. Specifically, the transfer will inevitably fail when:
The sendValue
function available in OpenZeppelin Contract’s Address library can be used to transfer the withdrawn Ether without being limited to 2300 gas units. Risks of reentrancy stemming from the use of this function can be mitigated by tightly following the "Check-effects-interactions" pattern and using OpenZeppelin Contract’s ReentrancyGuard contract. For further reference on why using Solidity’s transfer is no longer recommended, refer to these articles:
ethregistrar/ETHRegistrarController.sol#L183-L185
payable(msg.sender).transfer( msg.value - (price.base + price.premium) );
ethregistrar/ETHRegistrarController.sol#L204
if (msg.value > price.base) { payable(msg.sender).transfer(msg.value - price.base); }
ethregistrar/ETHRegistrarController.sol#L211
function withdraw() public { payable(owner()).transfer(address(this).balance); }
Manual review
Use Solidity's low-level call()
function or the sendValue
function available in OpenZeppelin Contract’s Address library to send Ether.
#0 - jefflau
2022-07-27T08:31:51Z
🌟 Selected for report: 0x29A
Also found by: Amithuddar, CRYP70, RedOneN, Sm4rty, benbaessler, berndartmueller, cccz, rbserver
When unwrapping a .eth domain via NameWrapper.unwrapETH2LD
, the specified newRegistrant
receives an ERC721 token representing the ownership of the domain. However, the newRegistrant
receiver receives the ERC721 token via the unsafe transferFrom
instead of the safe counterpart safeTransferFrom
. If the receiver is a contract and is not aware of incoming ERC721 tokens, the sent ERC721 tokens could be locked.
safeTransferFrom
checks that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked.
wrapper/NameWrapper.sol#L341-L345
function unwrapETH2LD( bytes32 labelhash, address newRegistrant, address newController ) public override onlyTokenOwner(_makeNode(ETH_NODE, labelhash)) { _unwrap(_makeNode(ETH_NODE, labelhash), newController); registrar.transferFrom( address(this), newRegistrant, uint256(labelhash) ); }
Manual review
Consider using safeTransferFrom()
within the NameWrapper.unwrapETH2LD
function.
#0 - jefflau
2022-07-22T08:16:29Z
🌟 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
78.881 USDC - $78.88
https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L170 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L270-L281
To prevent front running, the ETHRegistrarController
contract uses a two-step process to register names. First one has to call ETHRegistrarController.commit
with the desired configuration parameters and wait for minCommitmentAge
to pass by. Then a call to ETHRegistrarController.register
with the same parameters as in the previous step to finally register the name. This ETHRegistrarController.register
can be front-run without any consequences by anyone else. At least that's the case for registering the name.
If reverseRecord
is set to true
and the ETHRegistrarController.register
function is called by anyone else than the owner
, a reverse record with name
is set for the caller address msg.sender
instead of the owner
.
ETHRegistrarController.sol#L170
if (reverseRecord) { _setReverseRecord(name, resolver, msg.sender); // @audit-info the caller address `msg.sender` of `ETHRegistrarController.register` will be used as the `owner` for `_setReverseRecord` }
ETHRegistrarController._setReverseRecord
function _setReverseRecord( string memory name, address resolver, address owner ) internal { reverseRegistrar.setNameForAddr( msg.sender, owner, resolver, string.concat(name, ".eth") ); }
foo.eth
and calls ETHRegistrarController.commit
with the appropriate parameters and reverseRecord = true
minCommitmentAge
, Bob calls ETHRegistrarController.register
with the same parameters as in the step beforefoo.eth
is successfully registered (with Bob as the owner), however, Alice has now her address associated with a reverse record set to foo.eth
and Bob is missing the reverse record for his address.Copy-paste the following test into the TestEthRegistrarController.js
file and run the tests:
it.only("should set the reverse record of the account from someone else", async () => { const commitment = await controller.makeCommitment( "reverse", registrantAccount, REGISTRATION_TIME, secret, resolver.address, [], true, 0, 0 ); await controller.commit(commitment); await evm.advanceTime((await controller.minCommitmentAge()).toNumber()); await controller .connect(signers[2]) .register( "reverse", registrantAccount, REGISTRATION_TIME, secret, resolver.address, [], true, 0, 0, { value: BUFFERED_REGISTRATION_COST } ); const signer2Address = await signers[2].getAddress(); expect(await resolver.name(getReverseNode(signer2Address))).to.equal( "reverse.eth" ); // @audit-info caller "steals" reverse record expect(await resolver.name(getReverseNode(registrantAccount))).to.equal(""); // @audit-info owner of registered name lacks reverse record });
Manual review
Consider using the owner
instead of msg.sender
:
ETHRegistrarController.sol#L170
if (reverseRecord) { _setReverseRecord(name, resolver, owner); // @audit-info use `owner` instead of `msg.sender` }
ETHRegistrarController._setReverseRecord
function _setReverseRecord( string memory name, address resolver, address owner ) internal { reverseRegistrar.setNameForAddr( owner, // @audit-info use `owner` instead of `msg.sender` owner, resolver, string.concat(name, ".eth") ); }
#0 - jefflau
2022-07-22T08:15:29Z
The reverse for a name can be set to any name a user wants to, so despite this being a bug that it is possible, the name is still registered to the correct account and alice has just registered the name for bob and paid the registration fee, and has set her reverse to bob's name (but she could do that already, because setName can be set to anything that you would like).
#1 - dmvt
2022-08-03T14:54:20Z
I'm downgrading this to QA. There is a bug, but no negative impact on Bob.