Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contracts/script/foundry/DeployDOS.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ contract DeployDOS is Script, ERC1155Holder {
/// @notice Broadcasts a DOS Name Service deployment using environment configuration.
/// @dev Required env: `PRIVATE_KEY`. Optional env: `BENEFICIARY`, `PAYMENT_TOKEN`.
/// @return deployment The deployed contract set.
function run() external returns (Deployment memory deployment) {
function run() external virtual returns (Deployment memory deployment) {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(privateKey);
address beneficiary = vm.envOr("BENEFICIARY", deployer);
Expand Down Expand Up @@ -233,7 +233,7 @@ contract DeployDOS is Script, ERC1155Holder {
RegistryRolesLib.ROLE_SET_URI_ADMIN;
}

function _tldTokenRoles() internal pure returns (uint256) {
function _tldTokenRoles() internal pure virtual returns (uint256) {
return
RegistryRolesLib.ROLE_SET_SUBREGISTRY |
RegistryRolesLib.ROLE_SET_SUBREGISTRY_ADMIN |
Expand Down
141 changes: 141 additions & 0 deletions contracts/script/foundry/DeployDOSTestnet.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {DeployDOS} from "./DeployDOS.s.sol";

import {
DEFAULT_ROLE_BITMAP,
StandardRentPriceOracle
} from "~src/registrar/StandardRentPriceOracle.sol";
import {PermissionedRegistry} from "~src/registry/PermissionedRegistry.sol";
import {RegistryRolesLib} from "~src/registry/libraries/RegistryRolesLib.sol";
import {PermissionedResolver} from "~src/resolver/PermissionedResolver.sol";
import {PermissionedResolverLib} from "~src/resolver/libraries/PermissionedResolverLib.sol";
import {WrappedDOS} from "~src/testnet/WrappedDOS.sol";
import {LibLabel} from "~src/utils/LibLabel.sol";

/// @title Deploy DOS Name Service on testnet
/// @notice Deploys a standard wrapped-native WDOS token and the complete `.dos` ENSv2 stack.
contract DeployDOSTestnet is DeployDOS {
/// @notice Contracts produced by the DOS testnet deployment profile.
struct TestnetDeployment {
WrappedDOS wdos;
Deployment names;
}

/// @notice Broadcasts a testnet deployment using environment configuration.
/// @dev Required env: `PRIVATE_KEY`, `OWNER`. Optional env: `BENEFICIARY`.
function run() external override returns (Deployment memory deployment) {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address owner = vm.envAddress("OWNER");
address beneficiary = vm.envOr("BENEFICIARY", owner);
address broadcaster = vm.addr(privateKey);

vm.startBroadcast(privateKey);
TestnetDeployment memory testnetDeployment =
deployTestnet(broadcaster, owner, beneficiary, block.chainid);
vm.stopBroadcast();

deployment = testnetDeployment.names;
}

/// @notice Deploys WDOS and wires it into a DOS Name Service deployment.
/// @param initialOwner Account broadcasting deployment and temporary wiring calls.
/// @param owner Final protocol administrator.
function deployTestnet(address initialOwner, address owner, address beneficiary, uint256 chainId)
public
returns (TestnetDeployment memory deployment)
{
deployment.wdos = new WrappedDOS();
deployment.names = deploy(
initialOwner,
beneficiary,
IERC20(address(deployment.wdos)),
chainId
);
_handoff(deployment.names, initialOwner, owner);
}

function _handoff(Deployment memory deployment, address initialOwner, address owner) internal {
if (owner == initialOwner) {
return;
}
Comment on lines +61 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent accidental loss of ownership or administrative control over the entire protocol, it is highly recommended to validate that the final owner address is not address(0) before proceeding with the handoff.

    function _handoff(Deployment memory deployment, address initialOwner, address owner) internal {
        require(owner != address(0), "DeployDOSTestnet: owner cannot be zero address");
        if (owner == initialOwner) {
            return;
        }


_handoffTld(deployment.rootRegistry, initialOwner, owner, "dos");
_handoffTld(deployment.rootRegistry, initialOwner, owner, "reverse");
_handoffRoles(deployment.rootRegistry, initialOwner, owner, _rootRegistryRoles());
_handoffRoles(deployment.dosRegistry, initialOwner, owner, _dosRegistryRoles());
_handoffRoles(deployment.reverseRegistry, initialOwner, owner, _rootRegistryRoles());
_handoffRoles(deployment.priceOracle, initialOwner, owner, DEFAULT_ROLE_BITMAP);
_handoffRoles(
deployment.permissionedResolverImplementation,
initialOwner,
owner,
PermissionedResolverLib.ROLE_CAN_NAME | PermissionedResolverLib.ROLE_CAN_NAME_ADMIN
);
_handoffRoles(
deployment.userRegistryImplementation,
initialOwner,
owner,
RegistryRolesLib.ROLE_CAN_NAME | RegistryRolesLib.ROLE_CAN_NAME_ADMIN
);

deployment.contractNamer.transferOwnership(owner);
deployment.gatewayProvider.transferOwnership(owner);
deployment.dosRegistrar.transferOwnership(owner);
}
Comment on lines +61 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Critical Handoff Gap: TLD Token Ownership Retained by Deployer

During the deployment phase, deploy is called with initialOwner (the temporary broadcaster) as the owner parameter. This registers the core TLDs ("dos" and "reverse" on rootRegistry, and reverseLabel on reverseRegistry) under the ownership of initialOwner, minting the corresponding ERC1155 tokens to them.

However, in _handoff, these ERC1155 tokens are never transferred to the final owner. Because these tokens are registered without RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN (and with 0 roles for reverseLabel), they are permanently non-transferable by default. This leaves the temporary deployer as the permanent owner of the core TLDs, retaining ultimate admin control and preventing the final owner from fully managing them.

Suggested Fix:

  1. Modify DeployDOS.s.sol to include RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN in _tldTokenRoles() and when registering reverseLabel so that they can be transferred.
  2. In _handoff, transfer the tokens to the final owner using safeTransferFrom:
uint256 dosTokenId = deployment.rootRegistry.getTokenId(uint256(keccak256("dos")));
uint256 reverseTokenId = deployment.rootRegistry.getTokenId(uint256(keccak256("reverse")));
deployment.rootRegistry.safeTransferFrom(initialOwner, owner, dosTokenId, 1, "");
deployment.rootRegistry.safeTransferFrom(initialOwner, owner, reverseTokenId, 1, "");

uint256 reverseLabelTokenId = deployment.reverseRegistry.getTokenId(uint256(keccak256(bytes(deployment.reverseRegistrar.reverseLabel()))));
deployment.reverseRegistry.safeTransferFrom(initialOwner, owner, reverseLabelTokenId, 1, "");


function _handoffTld(
PermissionedRegistry registry,
address initialOwner,
address owner,
string memory label
)
internal
{
uint256 tokenId = registry.getTokenId(LibLabel.id(label));
registry.safeTransferFrom(initialOwner, owner, tokenId, 1, "");
}

function _handoffRoles(
PermissionedRegistry registry,
address initialOwner,
address owner,
uint256 roles
)
internal
{
registry.grantRootRoles(roles, owner);
registry.revokeRootRoles(roles, initialOwner);
}

function _handoffRoles(
StandardRentPriceOracle oracle,
address initialOwner,
address owner,
uint256 roles
)
internal
{
oracle.grantRootRoles(roles, owner);
oracle.revokeRootRoles(roles, initialOwner);
}

function _handoffRoles(
PermissionedResolver resolver,
address initialOwner,
address owner,
uint256 roles
)
internal
{
resolver.grantRootRoles(roles, owner);
resolver.revokeRootRoles(roles, initialOwner);
}

function _tldTokenRoles() internal pure override returns (uint256) {
return super._tldTokenRoles() | RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN;
}
}
18 changes: 18 additions & 0 deletions contracts/src/testnet/WrappedDOS.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {WETH} from "solady/tokens/WETH.sol";

/// @title Wrapped DOS
/// @notice Standard wrapped-native token used by DOS Chain testnet integrations.
contract WrappedDOS is WETH {
/// @inheritdoc WETH
function name() public pure override returns (string memory) {
return "Wrapped DOS";
}

/// @inheritdoc WETH
function symbol() public pure override returns (string memory) {
return "WDOS";
}
}
216 changes: 216 additions & 0 deletions contracts/test/unit/testnet/WrappedDOS.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {DeployDOS} from "../../../script/foundry/DeployDOS.s.sol";
import {DeployDOSTestnet} from "../../../script/foundry/DeployDOSTestnet.s.sol";

import {DOSRegistrar} from "~src/registrar/DOSRegistrar.sol";
import {IRegistry} from "~src/registry/interfaces/IRegistry.sol";
import {LibLabel} from "~src/utils/LibLabel.sol";
import {WrappedDOS} from "~src/testnet/WrappedDOS.sol";

contract WrappedDOSTest is Test {
WrappedDOS internal wdos;
address internal holder = makeAddr("holder");
address internal recipient = makeAddr("recipient");
DOSRegistrar internal testnetRegistrar;
WrappedDOS internal testnetPaymentToken;
address internal paymentRegistrant;
address internal paymentBeneficiary;

function setUp() external {
wdos = new WrappedDOS();
vm.deal(holder, 10 ether);
}

function test_metadataMatchesDOSNativeToken() external view {
assertEq(wdos.name(), "Wrapped DOS");
assertEq(wdos.symbol(), "WDOS");
assertEq(wdos.decimals(), 18);
}

function test_depositMintsOneToOneWrappedDOS() external {
vm.prank(holder);
wdos.deposit{value: 3 ether}();

assertEq(wdos.balanceOf(holder), 3 ether);
assertEq(address(wdos).balance, 3 ether);
assertEq(wdos.totalSupply(), 3 ether);
}

function test_transferAndWithdrawReturnNativeDOS() external {
vm.prank(holder);
wdos.deposit{value: 3 ether}();

vm.prank(holder);
wdos.transfer(recipient, 1 ether);

uint256 nativeBalanceBefore = recipient.balance;
vm.prank(recipient);
wdos.withdraw(1 ether);

assertEq(wdos.balanceOf(holder), 2 ether);
assertEq(wdos.balanceOf(recipient), 0);
assertEq(recipient.balance, nativeBalanceBefore + 1 ether);
assertEq(address(wdos).balance, 2 ether);
assertEq(wdos.totalSupply(), 2 ether);
}

function test_receiveMintsWrappedDOS() external {
vm.prank(holder);
(bool success, ) = address(wdos).call{value: 2 ether}("");

assertTrue(success);
assertEq(wdos.balanceOf(holder), 2 ether);
}

function test_testnetDeploymentWiresWrappedDOSAndExternalOwner() external {
DeployDOSTestnet deployer = new DeployDOSTestnet();
address protocolOwner = makeAddr("protocolOwner");
address beneficiary = makeAddr("beneficiary");

DeployDOSTestnet.TestnetDeployment memory deployment =
deployer.deployTestnet(address(deployer), protocolOwner, beneficiary, 3939);

assertEq(deployment.wdos.name(), "Wrapped DOS");
assertEq(deployment.wdos.symbol(), "WDOS");
assertEq(deployment.names.dosRegistrar.owner(), protocolOwner);
assertEq(deployment.names.dosRegistrar.BENEFICIARY(), beneficiary);
assertEq(deployment.names.rootRegistry.roles(0, address(deployer)), 0);
assertEq(deployment.names.dosRegistry.roles(0, address(deployer)), 0);
assertEq(deployment.names.reverseRegistry.roles(0, address(deployer)), 0);
assertEq(deployment.names.priceOracle.roles(0, address(deployer)), 0);
assertEq(deployment.names.permissionedResolverImplementation.roles(0, address(deployer)), 0);
assertEq(deployment.names.userRegistryImplementation.roles(0, address(deployer)), 0);
assertTrue(deployment.names.rootRegistry.roles(0, protocolOwner) != 0);
assertTrue(deployment.names.dosRegistry.roles(0, protocolOwner) != 0);
assertTrue(deployment.names.reverseRegistry.roles(0, protocolOwner) != 0);
assertTrue(deployment.names.priceOracle.roles(0, protocolOwner) != 0);
assertTrue(deployment.names.permissionedResolverImplementation.roles(0, protocolOwner) != 0);
assertTrue(deployment.names.userRegistryImplementation.roles(0, protocolOwner) != 0);

uint256 dosTokenId = deployment.names.rootRegistry.getTokenId(LibLabel.id("dos"));
uint256 reverseTokenId = deployment.names.rootRegistry.getTokenId(LibLabel.id("reverse"));
assertEq(deployment.names.rootRegistry.ownerOf(dosTokenId), protocolOwner);
assertEq(deployment.names.rootRegistry.ownerOf(reverseTokenId), protocolOwner);
assertEq(deployment.names.rootRegistry.roles(dosTokenId, address(deployer)), 0);
assertEq(deployment.names.rootRegistry.roles(reverseTokenId, address(deployer)), 0);
assertTrue(deployment.names.rootRegistry.roles(dosTokenId, protocolOwner) != 0);
assertTrue(deployment.names.rootRegistry.roles(reverseTokenId, protocolOwner) != 0);

vm.expectRevert();
vm.prank(address(deployer));
deployment.names.rootRegistry.setResolver(dosTokenId, address(1));

vm.expectRevert();
vm.prank(address(deployer));
deployment.names.rootRegistry.setSubregistry(
reverseTokenId,
IRegistry(address(deployment.names.dosRegistry))
);

(uint128 numer, uint128 denom) =
deployment.names.priceOracle.getPaymentTokenRatio(IERC20(address(deployment.wdos)));
assertEq(numer, 1e6);
assertEq(denom, 1);
}

function test_runUsesCanonicalTestnetFlow() external {
DeployDOSTestnet deployer = new DeployDOSTestnet();
uint256 privateKey = 0xA11CE;
address protocolOwner = makeAddr("runProtocolOwner");
address beneficiary = makeAddr("runBeneficiary");

vm.setEnv("PRIVATE_KEY", vm.toString(privateKey));
vm.setEnv("OWNER", vm.toString(protocolOwner));
vm.setEnv("BENEFICIARY", vm.toString(beneficiary));

DeployDOS.Deployment memory deployment = deployer.run();

assertEq(deployment.dosRegistrar.owner(), protocolOwner);
assertEq(deployment.dosRegistrar.BENEFICIARY(), beneficiary);
}

function test_testnetRegistrationUsesWrappedDOSAndPaysBeneficiary() external {
DeployDOSTestnet deployer = new DeployDOSTestnet();
address protocolOwner = makeAddr("paymentProtocolOwner");
paymentBeneficiary = makeAddr("paymentBeneficiary");
paymentRegistrant = makeAddr("registrant");

DeployDOSTestnet.TestnetDeployment memory deployment =
deployer.deployTestnet(address(deployer), protocolOwner, paymentBeneficiary, 3939);
testnetRegistrar = deployment.names.dosRegistrar;
testnetPaymentToken = deployment.wdos;

vm.deal(paymentRegistrant, 1_000 ether);
vm.startPrank(paymentRegistrant);
testnetPaymentToken.deposit{value: 1_000 ether}();
testnetPaymentToken.approve(address(testnetRegistrar), type(uint256).max);
vm.stopPrank();

vm.warp(testnetRegistrar.GRACE_PERIOD() + 1);

_registerAndRenew("a", 100_00002528e10);
_registerAndRenew("ab", 50_00001264e10);
_registerAndRenew("abc", 10_000002528e9);

assertEq(deployment.wdos.balanceOf(address(deployer)), 0);
assertEq(deployment.wdos.balanceOf(protocolOwner), 0);
}

function _registerAndRenew(string memory label, uint256 expectedPrice) internal {
uint64 duration = 365 days;
bytes32 secret = keccak256(bytes(label));
bytes32 commitment =
testnetRegistrar.makeCommitment(
label,
paymentRegistrant,
secret,
IRegistry(address(0)),
address(0),
duration,
bytes32(0)
);

vm.prank(paymentRegistrant);
testnetRegistrar.commit(commitment);
vm.warp(block.timestamp + testnetRegistrar.MIN_COMMITMENT_AGE());

(uint256 base, uint256 premium) =
testnetRegistrar.getRegisterPrice(label, duration, IERC20(address(testnetPaymentToken)));
assertEq(base, expectedPrice);
assertEq(premium, 0);

uint256 beneficiaryBefore = testnetPaymentToken.balanceOf(paymentBeneficiary);
vm.prank(paymentRegistrant);
testnetRegistrar.register(
label,
paymentRegistrant,
secret,
IRegistry(address(0)),
address(0),
duration,
IERC20(address(testnetPaymentToken)),
bytes32(0)
);
assertEq(
testnetPaymentToken.balanceOf(paymentBeneficiary),
beneficiaryBefore + expectedPrice
);

uint256 renewalPrice =
testnetRegistrar.getRenewPrice(label, duration, IERC20(address(testnetPaymentToken)));
assertEq(renewalPrice, expectedPrice);

vm.prank(paymentRegistrant);
testnetRegistrar.renew(label, duration, IERC20(address(testnetPaymentToken)), bytes32(0));
assertEq(
testnetPaymentToken.balanceOf(paymentBeneficiary),
beneficiaryBefore + 2 * expectedPrice
);
}
}
Loading