-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add DOS testnet ENSv2 deployment #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
|
|
||
| _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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical Handoff Gap: TLD Token Ownership Retained by DeployerDuring the deployment phase, However, in Suggested Fix:
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; | ||
| } | ||
| } | ||
| 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"; | ||
| } | ||
| } |
| 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 | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent accidental loss of ownership or administrative control over the entire protocol, it is highly recommended to validate that the final
owneraddress is notaddress(0)before proceeding with the handoff.