diff --git a/contracts/script/foundry/DeployDOS.s.sol b/contracts/script/foundry/DeployDOS.s.sol index d2d0ec403..1698cd2f1 100644 --- a/contracts/script/foundry/DeployDOS.s.sol +++ b/contracts/script/foundry/DeployDOS.s.sol @@ -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); @@ -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 | diff --git a/contracts/script/foundry/DeployDOSTestnet.s.sol b/contracts/script/foundry/DeployDOSTestnet.s.sol new file mode 100644 index 000000000..99948631a --- /dev/null +++ b/contracts/script/foundry/DeployDOSTestnet.s.sol @@ -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; + } + + _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); + } + + 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; + } +} diff --git a/contracts/src/testnet/WrappedDOS.sol b/contracts/src/testnet/WrappedDOS.sol new file mode 100644 index 000000000..ffa164702 --- /dev/null +++ b/contracts/src/testnet/WrappedDOS.sol @@ -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"; + } +} diff --git a/contracts/test/unit/testnet/WrappedDOS.t.sol b/contracts/test/unit/testnet/WrappedDOS.t.sol new file mode 100644 index 000000000..a531db408 --- /dev/null +++ b/contracts/test/unit/testnet/WrappedDOS.t.sol @@ -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 + ); + } +}