diff --git a/contracts/script/foundry/DeployDOSMainnet.s.sol b/contracts/script/foundry/DeployDOSMainnet.s.sol new file mode 100644 index 000000000..e0d7f5fa9 --- /dev/null +++ b/contracts/script/foundry/DeployDOSMainnet.s.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {DeployDOS} from "./DeployDOS.s.sol"; +import {DeployDOSTestnet} from "./DeployDOSTestnet.s.sol"; + +/// @title Deploy DOS Name Service on mainnet +/// @notice Deploys the complete `.dos` ENSv2 stack against the canonical WDOS contract. +contract DeployDOSMainnet is DeployDOSTestnet { + uint256 internal constant MAINNET_CHAIN_ID = 7979; + address internal constant MAINNET_DEPLOYER = 0x99999e454138f6be73E2bE82c890bc5765749999; + address internal constant MAINNET_OWNER = 0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD; + uint8 internal constant MAINNET_WDOS_DECIMALS = 18; + + error MainnetUnexpectedChain(uint256 actual, uint256 expected); + error MainnetUnexpectedDeployer(address actual, address expected); + error MainnetUnexpectedOwner(address actual, address expected); + error MainnetUnexpectedBeneficiary(address actual, address expected); + error MainnetInsufficientDeploymentBalance(uint256 actual, uint256 required); + error MissingPaymentTokenCode(address paymentToken); + error UnexpectedPaymentTokenDecimals(uint8 actual, uint8 expected); + + /// @notice Contracts produced by the DOS mainnet deployment profile. + struct MainnetDeployment { + IERC20 paymentToken; + DeployDOS.Deployment names; + } + + /// @notice Broadcasts the canonical Mainnet 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); + + preflightMainnet(broadcaster, owner, beneficiary, DEFAULT_WDOS); + vm.startBroadcast(privateKey); + MainnetDeployment memory mainnetDeployment = + deployMainnet(broadcaster, owner, beneficiary, IERC20(DEFAULT_WDOS), block.chainid); + vm.stopBroadcast(); + + deployment = mainnetDeployment.names; + } + + /// @notice Fails before broadcasting if the Mainnet deployment configuration is not canonical. + function preflightMainnet( + address broadcaster, + address owner, + address beneficiary, + address paymentToken + ) + public + view + { + if (block.chainid != MAINNET_CHAIN_ID) { + revert MainnetUnexpectedChain(block.chainid, MAINNET_CHAIN_ID); + } + if (broadcaster != MAINNET_DEPLOYER) { + revert MainnetUnexpectedDeployer(broadcaster, MAINNET_DEPLOYER); + } + if (owner != MAINNET_OWNER) { + revert MainnetUnexpectedOwner(owner, MAINNET_OWNER); + } + if (beneficiary != MAINNET_OWNER) { + revert MainnetUnexpectedBeneficiary(beneficiary, MAINNET_OWNER); + } + if (broadcaster.balance < MIN_DEPLOYMENT_BALANCE) { + revert MainnetInsufficientDeploymentBalance(broadcaster.balance, MIN_DEPLOYMENT_BALANCE); + } + if (paymentToken.code.length == 0) { + revert MissingPaymentTokenCode(paymentToken); + } + uint8 decimals = IERC20Metadata(paymentToken).decimals(); + if (decimals != MAINNET_WDOS_DECIMALS) { + revert UnexpectedPaymentTokenDecimals(decimals, MAINNET_WDOS_DECIMALS); + } + } + + /// @notice Deploys ENSv2 against an existing payment token and hands all control to the owner. + function deployMainnet( + address initialOwner, + address owner, + address beneficiary, + IERC20 paymentToken, + uint256 chainId + ) + public + returns (MainnetDeployment memory deployment) + { + deployment.paymentToken = paymentToken; + deployment.names = deploy(initialOwner, beneficiary, paymentToken, chainId); + _registerBensSmokeName(deployment.names, initialOwner, owner, chainId); + _handoff(deployment.names, initialOwner, owner); + } +} diff --git a/contracts/script/foundry/DeployDOSTestnet.s.sol b/contracts/script/foundry/DeployDOSTestnet.s.sol index 080ec625b..b54fcd3db 100644 --- a/contracts/script/foundry/DeployDOSTestnet.s.sol +++ b/contracts/script/foundry/DeployDOSTestnet.s.sol @@ -44,7 +44,7 @@ contract DeployDOSTestnet is DeployDOS { /// @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) { + function run() external virtual override returns (Deployment memory deployment) { uint256 privateKey = vm.envUint("PRIVATE_KEY"); address owner = vm.envAddress("OWNER"); address beneficiary = vm.envOr("BENEFICIARY", owner); diff --git a/contracts/script/foundry/Invoke-DeployDOSMainnet.ps1 b/contracts/script/foundry/Invoke-DeployDOSMainnet.ps1 new file mode 100644 index 000000000..446a75fff --- /dev/null +++ b/contracts/script/foundry/Invoke-DeployDOSMainnet.ps1 @@ -0,0 +1,125 @@ +[CmdletBinding()] +param( + [string]$RpcUrl = "https://main.doschain.com", + [switch]$Broadcast +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$expectedRpcUrl = "https://main.doschain.com" +$expectedChainId = 7979 +$expectedGenesisHash = "0x3b5fbd6089c79e21843f16384316ad75de4951f8bb2d0f26e3ce12e984e2e82b" +$expectedDeployer = "0x99999e454138f6be73e2be82c890bc5765749999" +$expectedOwner = "0x310bc061214ee89af5cfb28a6ebf96c5436fa3cd" +$expectedPaymentToken = "0x1111111111111111111111111111111111111111" +$minimumBalanceWei = [System.Numerics.BigInteger]::Parse("1000000000000000000") + +function Resolve-FoundryCommand { + param([Parameter(Mandatory)][string]$Name) + + $command = Get-Command $Name -ErrorAction SilentlyContinue + if ($null -ne $command) { + return $command.Source + } + + $bundled = Join-Path $env:USERPROFILE ".foundry\bin\$Name.exe" + if (Test-Path -LiteralPath $bundled) { + return $bundled + } + + throw "$Name was not found in PATH or the Foundry installation directory" +} + +if ($RpcUrl.TrimEnd("/") -ne $expectedRpcUrl) { + throw "RPC URL must be $expectedRpcUrl" +} +if ([string]::IsNullOrWhiteSpace($env:PRIVATE_KEY)) { + throw "PRIVATE_KEY is required" +} +if ([string]::IsNullOrWhiteSpace($env:OWNER)) { + throw "OWNER is required" +} + +$privateKey = $env:PRIVATE_KEY.Trim() +if ($privateKey -match "^[0-9a-fA-F]{64}$") { + $privateKey = "0x$privateKey" +} +if ($privateKey -notmatch "^0x[0-9a-fA-F]{64}$") { + throw "PRIVATE_KEY must be a 32-byte hexadecimal value" +} +$env:PRIVATE_KEY = $privateKey + +$owner = $env:OWNER.Trim().ToLowerInvariant() +$beneficiary = if ([string]::IsNullOrWhiteSpace($env:BENEFICIARY)) { + $env:OWNER +} else { + $env:BENEFICIARY +} +$beneficiary = $beneficiary.Trim().ToLowerInvariant() +if ($owner -ne $expectedOwner) { + throw "OWNER does not match the canonical DOS Names owner" +} +if ($beneficiary -ne $expectedOwner) { + throw "BENEFICIARY does not match the canonical DOS Names beneficiary" +} +$env:BENEFICIARY = $beneficiary + +$cast = Resolve-FoundryCommand -Name "cast" +$forge = Resolve-FoundryCommand -Name "forge" + +$chainId = [int]((& $cast chain-id --rpc-url $RpcUrl).Trim()) +if ($LASTEXITCODE -ne 0 -or $chainId -ne $expectedChainId) { + throw "RPC chain ID does not match DOS Mainnet" +} + +$genesis = (& $cast block 0 --rpc-url $RpcUrl --json | ConvertFrom-Json).hash.ToLowerInvariant() +if ($LASTEXITCODE -ne 0 -or $genesis -ne $expectedGenesisHash) { + throw "RPC genesis hash does not match DOS Mainnet" +} + +$paymentTokenCode = (& $cast code $expectedPaymentToken --rpc-url $RpcUrl).Trim() +if ($LASTEXITCODE -ne 0 -or $paymentTokenCode -eq "0x" -or $paymentTokenCode -eq "0x0") { + throw "Canonical WDOS has no Mainnet bytecode" +} +$paymentTokenDecimals = [int]((& $cast call $expectedPaymentToken "decimals()(uint8)" --rpc-url $RpcUrl).Trim()) +if ($LASTEXITCODE -ne 0 -or $paymentTokenDecimals -ne 18) { + throw "Canonical WDOS must use 18 decimals" +} + +$balanceOutput = & $cast balance $expectedDeployer --rpc-url $RpcUrl +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($balanceOutput)) { + throw "Unable to read the canonical deployer balance" +} +$balance = [System.Numerics.BigInteger]::Parse($balanceOutput.Trim()) +if ($balance -lt $minimumBalanceWei) { + throw "Canonical deployer balance is below the 1 DOS deployment floor" +} + +$contractsRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..") +Push-Location $contractsRoot +try { + & $forge script "script/foundry/DeployDOSMainnet.s.sol:DeployDOSMainnet" ` + --rpc-url $RpcUrl ` + --slow + if ($LASTEXITCODE -ne 0) { + throw "Foundry simulation failed" + } + + if (-not $Broadcast) { + Write-Output "DOS_MAINNET_ENSV2_SIMULATION_OK" + return + } + + & $forge script "script/foundry/DeployDOSMainnet.s.sol:DeployDOSMainnet" ` + --rpc-url $RpcUrl ` + --broadcast ` + --slow + if ($LASTEXITCODE -ne 0) { + throw "Foundry broadcast failed" + } + Write-Output "DOS_MAINNET_ENSV2_BROADCAST_OK" +} +finally { + Pop-Location +} diff --git a/contracts/test/e2e/mainnetDeployWrapper.test.ts b/contracts/test/e2e/mainnetDeployWrapper.test.ts new file mode 100644 index 000000000..f82bd83fa --- /dev/null +++ b/contracts/test/e2e/mainnetDeployWrapper.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "bun:test"; +import { resolve } from "node:path"; + +const wrapperPath = resolve( + import.meta.dir, + "../../script/foundry/Invoke-DeployDOSMainnet.ps1", +); + +describe("DOS Mainnet deployment wrapper", () => { + it("never passes the private key through process arguments", async () => { + const wrapper = await Bun.file(wrapperPath).text(); + + expect(wrapper).not.toContain("--private-key"); + expect(wrapper).not.toContain("wallet address"); + expect(wrapper).toContain('$env:PRIVATE_KEY = $privateKey'); + expect(wrapper).toContain( + 'forge script "script/foundry/DeployDOSMainnet.s.sol:DeployDOSMainnet"', + ); + }); +}); diff --git a/contracts/test/unit/mainnet/DeployDOSMainnet.t.sol b/contracts/test/unit/mainnet/DeployDOSMainnet.t.sol new file mode 100644 index 000000000..56c113822 --- /dev/null +++ b/contracts/test/unit/mainnet/DeployDOSMainnet.t.sol @@ -0,0 +1,207 @@ +// 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 {NameCoder} from "@ens/contracts/utils/NameCoder.sol"; + +import {DeployDOSMainnet} from "../../../script/foundry/DeployDOSMainnet.s.sol"; + +import {IRegistry} from "~src/registry/interfaces/IRegistry.sol"; +import {PermissionedResolver} from "~src/resolver/PermissionedResolver.sol"; +import {WrappedDOS} from "~src/testnet/WrappedDOS.sol"; +import {MockERC20} from "~test/mocks/MockERC20.sol"; +import {LibLabel} from "~src/utils/LibLabel.sol"; + +contract DeployDOSMainnetTest is Test { + uint256 internal constant MAINNET_CHAIN_ID = 7979; + address internal constant MAINNET_DEPLOYER = 0x99999e454138f6be73E2bE82c890bc5765749999; + address internal constant PROTOCOL_OWNER = 0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD; + address internal constant CANONICAL_WDOS = 0x1111111111111111111111111111111111111111; + + function test_mainnetDeploymentUsesExistingWDOSAndHandsOffControl() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + + DeployDOSMainnet.MainnetDeployment memory deployment = + deployer.deployMainnet( + address(deployer), + PROTOCOL_OWNER, + PROTOCOL_OWNER, + IERC20(address(paymentToken)), + 7979 + ); + + assertEq(address(deployment.paymentToken), address(paymentToken)); + assertEq(deployment.names.dosRegistrar.owner(), PROTOCOL_OWNER); + assertEq(deployment.names.dosRegistrar.BENEFICIARY(), PROTOCOL_OWNER); + 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); + + uint256 dosTokenId = deployment.names.rootRegistry.getTokenId(LibLabel.id("dos")); + uint256 reverseTokenId = deployment.names.rootRegistry.getTokenId(LibLabel.id("reverse")); + uint256 smokeTokenId = deployment.names.dosRegistry.getTokenId(LibLabel.id("bens-smoke")); + assertEq(deployment.names.rootRegistry.ownerOf(dosTokenId), PROTOCOL_OWNER); + assertEq(deployment.names.rootRegistry.ownerOf(reverseTokenId), PROTOCOL_OWNER); + assertEq(deployment.names.dosRegistry.ownerOf(smokeTokenId), PROTOCOL_OWNER); + assertLe(deployment.names.dosRegistry.getExpiry(smokeTokenId), 253402300799); + + address resolverAddress = deployment.names.dosRegistry.getResolver("bens-smoke"); + bytes32 node = NameCoder.namehash(NameCoder.encode("bens-smoke.dos"), 0); + assertEq(PermissionedResolver(resolverAddress).addr(node), address(deployer)); + assertEq(deployment.names.reverseRegistrar.nameForAddr(address(deployer)), "bens-smoke.dos"); + + vm.expectRevert(); + vm.prank(address(deployer)); + deployment.names.rootRegistry.setSubregistry( + reverseTokenId, + IRegistry(address(deployment.names.dosRegistry)) + ); + } + + function test_preflightAcceptsCanonicalMainnetConfiguration() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(MAINNET_DEPLOYER, 1 ether); + + deployer.preflightMainnet( + MAINNET_DEPLOYER, + PROTOCOL_OWNER, + PROTOCOL_OWNER, + address(paymentToken) + ); + } + + function test_preflightRejectsWrongChain() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + vm.chainId(3939); + vm.deal(MAINNET_DEPLOYER, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector(DeployDOSMainnet.MainnetUnexpectedChain.selector, 3939, 7979) + ); + deployer.preflightMainnet( + MAINNET_DEPLOYER, + PROTOCOL_OWNER, + PROTOCOL_OWNER, + address(paymentToken) + ); + } + + function test_preflightRejectsWrongSigner() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + address wrongSigner = makeAddr("wrongSigner"); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(wrongSigner, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector( + DeployDOSMainnet.MainnetUnexpectedDeployer.selector, + wrongSigner, + MAINNET_DEPLOYER + ) + ); + deployer.preflightMainnet(wrongSigner, PROTOCOL_OWNER, PROTOCOL_OWNER, address(paymentToken)); + } + + function test_preflightRejectsWrongOwner() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + address wrongOwner = makeAddr("wrongOwner"); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(MAINNET_DEPLOYER, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector( + DeployDOSMainnet.MainnetUnexpectedOwner.selector, + wrongOwner, + PROTOCOL_OWNER + ) + ); + deployer.preflightMainnet( + MAINNET_DEPLOYER, + wrongOwner, + PROTOCOL_OWNER, + address(paymentToken) + ); + } + + function test_preflightRejectsWrongBeneficiary() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + address wrongBeneficiary = makeAddr("wrongBeneficiary"); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(MAINNET_DEPLOYER, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector( + DeployDOSMainnet.MainnetUnexpectedBeneficiary.selector, + wrongBeneficiary, + PROTOCOL_OWNER + ) + ); + deployer.preflightMainnet( + MAINNET_DEPLOYER, + PROTOCOL_OWNER, + wrongBeneficiary, + address(paymentToken) + ); + } + + function test_preflightRejectsInsufficientBalance() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + WrappedDOS paymentToken = new WrappedDOS(); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(MAINNET_DEPLOYER, 1 ether - 1); + + vm.expectRevert( + abi.encodeWithSelector( + DeployDOSMainnet.MainnetInsufficientDeploymentBalance.selector, + 1 ether - 1, + 1 ether + ) + ); + deployer.preflightMainnet( + MAINNET_DEPLOYER, + PROTOCOL_OWNER, + PROTOCOL_OWNER, + address(paymentToken) + ); + } + + function test_preflightRejectsWrongWDOSDecimals() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + MockERC20 paymentToken = new MockERC20("Wrong decimals", 17); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(MAINNET_DEPLOYER, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector(DeployDOSMainnet.UnexpectedPaymentTokenDecimals.selector, 17, 18) + ); + deployer.preflightMainnet( + MAINNET_DEPLOYER, + PROTOCOL_OWNER, + PROTOCOL_OWNER, + address(paymentToken) + ); + } + + function test_preflightRejectsMissingWDOSCode() external { + DeployDOSMainnet deployer = new DeployDOSMainnet(); + vm.chainId(MAINNET_CHAIN_ID); + vm.deal(MAINNET_DEPLOYER, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector(DeployDOSMainnet.MissingPaymentTokenCode.selector, CANONICAL_WDOS) + ); + deployer.preflightMainnet(MAINNET_DEPLOYER, PROTOCOL_OWNER, PROTOCOL_OWNER, CANONICAL_WDOS); + } +} diff --git a/subgraph/dos-names/src/registry.ts b/subgraph/dos-names/src/registry.ts index df8d1ecd3..4b8d82e93 100644 --- a/subgraph/dos-names/src/registry.ts +++ b/subgraph/dos-names/src/registry.ts @@ -93,6 +93,7 @@ export function handleLabelRegistered(event: LabelRegisteredEvent): void { // Create account let account = createOrLoadAccount(owner.toHexString()); + let registryAccount = createOrLoadAccount(event.address.toHexString()); // Create or load the .dos TLD domain (parent) let parentDomain = createOrLoadDomain(DOS_NODE); @@ -116,7 +117,10 @@ export function handleLabelRegistered(event: LabelRegisteredEvent): void { domain.isMigrated = true; } - domain.owner = account.id; + // BENS follows the canonical ENS subgraph model for wrapped names: + // Domain.owner is the ERC1155 wrapper/registry contract while + // Domain.wrappedOwner and WrappedDomain.owner are the token holder. + domain.owner = registryAccount.id; domain.registrant = account.id; domain.parent = DOS_NODE; domain.labelName = label; @@ -306,7 +310,6 @@ function applyOwnershipTransfer( return; } - domain.owner = account.id; domain.registrant = account.id; domain.wrappedOwner = account.id; domain.save(); diff --git a/subgraph/dos-names/src/subregistry.ts b/subgraph/dos-names/src/subregistry.ts index 7b1c45cb3..f14140477 100644 --- a/subgraph/dos-names/src/subregistry.ts +++ b/subgraph/dos-names/src/subregistry.ts @@ -178,6 +178,7 @@ export function materializeRegistryChild( .keccak256(concat(changetype(parentNode), child.labelhash)) .toHexString(); let account = createOrLoadAccount(child.owner.toHexString()); + let registryAccount = createOrLoadAccount(registry.toHexString()); let domain = Domain.load(node); let isNew = domain === null; if (domain === null) { @@ -189,7 +190,7 @@ export function materializeRegistryChild( domain.isMigrated = true; } - domain.owner = account.id; + domain.owner = registryAccount.id; domain.registrant = account.id; domain.wrappedOwner = account.id; domain.parent = parentId; diff --git a/subgraph/dos-names/src/userRegistry.ts b/subgraph/dos-names/src/userRegistry.ts index ca20c5c9b..b95966fea 100644 --- a/subgraph/dos-names/src/userRegistry.ts +++ b/subgraph/dos-names/src/userRegistry.ts @@ -226,7 +226,6 @@ function applyOwnershipTransfer( if (domain === null) { return; } - domain.owner = account.id; domain.registrant = account.id; domain.wrappedOwner = account.id; domain.save(); diff --git a/subgraph/dos-names/tests/registry.test.ts b/subgraph/dos-names/tests/registry.test.ts index 0cd4af46a..c32c84f6e 100644 --- a/subgraph/dos-names/tests/registry.test.ts +++ b/subgraph/dos-names/tests/registry.test.ts @@ -61,6 +61,7 @@ import { Domain, RegistryPath } from "../src/types/schema"; const OWNER = "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"; const NEW_OWNER = "0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41"; const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const DOS_REGISTRY = "0x5555555555555555555555555555555555555555"; const USER_REGISTRY = "0x1111111111111111111111111111111111111111"; const RESOLVER = "0x2222222222222222222222222222222222222222"; const NESTED_REGISTRY = "0x3333333333333333333333333333333333333333"; @@ -84,6 +85,7 @@ const SECOND_BOB_DOS = function registration(tokenId: i32): LabelRegistered { let mock = newMockEvent(); + mock.address = Address.fromString(DOS_REGISTRY); let event = new LabelRegistered( mock.address, mock.logIndex, @@ -646,7 +648,9 @@ test("token regeneration preserves the canonical domain mapping", () => { handleTokenRegenerated(regeneration(123, 456)); handleTransferSingle(transfer(456)); - assert.fieldEquals("Domain", ALICE_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", ALICE_DOS, "owner", DOS_REGISTRY); + assert.fieldEquals("Domain", ALICE_DOS, "wrappedOwner", NEW_OWNER); + assert.fieldEquals("WrappedDomain", ALICE_DOS, "owner", NEW_OWNER); assert.fieldEquals("Domain", ALICE_DOS, "tokenId", "456"); assert.fieldEquals( "TokenToDomain", @@ -665,7 +669,8 @@ test("zero-value transfers do not change top-level ownership", () => { handleTransferSingle(transfer(123, 0)); - assert.fieldEquals("Domain", ALICE_DOS, "owner", OWNER); + assert.fieldEquals("Domain", ALICE_DOS, "owner", DOS_REGISTRY); + assert.fieldEquals("Domain", ALICE_DOS, "wrappedOwner", OWNER); assert.entityCount("Transfer", 0); }); @@ -674,7 +679,8 @@ test("batch transfers update mapped top-level names and ignore zero values", () handleTransferBatch(batchTransfer([123, 999], [1, 0])); - assert.fieldEquals("Domain", ALICE_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", ALICE_DOS, "owner", DOS_REGISTRY); + assert.fieldEquals("Domain", ALICE_DOS, "wrappedOwner", NEW_OWNER); assert.entityCount("Transfer", 1); }); @@ -696,7 +702,8 @@ test("a user-registry token transfer updates the subname owner", () => { handleUserRegistryTransferSingle(userRegistryTransfer(456)); - assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", USER_REGISTRY); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "wrappedOwner", NEW_OWNER); assert.fieldEquals("WrappedDomain", SUB_ALICE_DOS, "owner", NEW_OWNER); assert.fieldEquals("Registration", SUB_ALICE_DOS, "registrant", NEW_OWNER); }); @@ -708,7 +715,8 @@ test("zero-value user-registry transfers do not change ownership", () => { handleUserRegistryTransferSingle(userRegistryTransfer(456, 0)); - assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", OWNER); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", USER_REGISTRY); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "wrappedOwner", OWNER); }); test("batch transfers update mapped user-registry names", () => { @@ -718,7 +726,8 @@ test("batch transfers update mapped user-registry names", () => { handleUserRegistryTransferBatch(userRegistryBatchTransfer([456, 999], [1, 0])); - assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", USER_REGISTRY); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "wrappedOwner", NEW_OWNER); assert.fieldEquals("WrappedDomain", SUB_ALICE_DOS, "owner", NEW_OWNER); }); @@ -759,7 +768,8 @@ test("detached registry sources retain current state for a later attachment", () ); assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", ZERO_ADDRESS); - assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", USER_REGISTRY); + assert.fieldEquals("Domain", SUB_BOB_DOS, "wrappedOwner", NEW_OWNER); }); test("attaching a shared registry backfills existing children for the new parent", () => { @@ -786,7 +796,7 @@ test("attaching a shared registry backfills existing children for the new parent assert.entityCount("TokenToDomain", 3); assert.fieldEquals("Domain", SUB_BOB_DOS, "name", "sub.bob.dos"); - assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", OWNER); + assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", USER_REGISTRY); }); test("backfilling multiple children preserves each ownership event", () => { @@ -814,8 +824,8 @@ test("backfilling multiple children preserves each ownership event", () => { [] ); - assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", OWNER); - assert.fieldEquals("Domain", SECOND_BOB_DOS, "owner", OWNER); + assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", USER_REGISTRY); + assert.fieldEquals("Domain", SECOND_BOB_DOS, "owner", USER_REGISTRY); assert.entityCount("NewOwner", 5); }); @@ -922,8 +932,8 @@ test("shared registry events retain one history row per parent context", () => { handleUserRegistryTransferSingle(userRegistryTransfer(456)); assert.entityCount("Transfer", 2); - assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", NEW_OWNER); - assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", USER_REGISTRY); + assert.fieldEquals("Domain", SUB_BOB_DOS, "owner", USER_REGISTRY); }); test("a user-registry unregistration retires the scoped token mapping", () => { @@ -1004,7 +1014,7 @@ test("user-registry token regeneration keeps later transfers attached to the sub handleUserRegistryTransferSingle(userRegistryTransfer(789)); assert.fieldEquals("Domain", SUB_ALICE_DOS, "tokenId", "789"); - assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", NEW_OWNER); + assert.fieldEquals("Domain", SUB_ALICE_DOS, "owner", USER_REGISTRY); assert.fieldEquals( "TokenToDomain", USER_REGISTRY