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
39 changes: 38 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,47 @@ jobs:
- name: Format
run: bun --filter contracts format:check

subgraph-tests:
name: DOS Names Subgraph
# Matchstick 0.6.0 publishes a Linux 22 binary but no Linux 24 binary.
runs-on: ubuntu-22.04
defaults:
run:
working-directory: subgraph/dos-names
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: subgraph/dos-names/package-lock.json

- name: Install dependencies
run: npm ci

- name: Test manifest rendering
run: npm run test:render

- name: Render test manifest
run: npm run render:test

- name: Generate subgraph types
run: npm run codegen

- name: Test mappings
run: npm test

- name: Render canonical deployment manifest
run: npm run render:deployment

- name: Build canonical deployment subgraph
run: npm run build

publish-docker:
name: Publish Docker Image
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.ref_type == 'branch')
needs: [check-types, e2e-tests, forge-tests, hardhat-tests, lint]
needs: [check-types, e2e-tests, forge-tests, hardhat-tests, lint, subgraph-tests]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
35 changes: 35 additions & 0 deletions contracts/deployments/dos-testnet-3939.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"environment": "dos-testnet",
"chainId": 3939,
"deploymentBlock": 120,
"finalDeploymentBlock": 166,
"deployedAt": "2026-08-12T15:49:48Z",
"deployer": "0x99999e454138f6be73E2bE82c890bc5765749999",
"owner": "0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD",
"beneficiary": "0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD",
"smokeName": "bens-smoke.dos",
"smokeResolvedAddress": "0x99999e454138f6be73E2bE82c890bc5765749999",
"transactions": {
"first": "0xc0f8e5390dab49911ce5f0f4c0a035e3251033c7d502f3db2632e389ef3ab599",
"last": "0xa3f4e38ec91c8db369df6afa40eea3e28f73a858e3a4c1bbceca3a60b38e6d0a",
"count": 47
},
"contracts": {
"wrappedDOS": "0x4bb2dcea9e18604741287a12d87abd9de793e3f4",
"contractNamerImplementation": "0xdd20fc2290cf664eccd3414799a9f024bdea531c",
"contractNamer": "0x64A04D812cdc62971f7E446E15a53c3C57f11418",
"verifiableFactory": "0xC9A8C00499Ef4f20Fffad7114C0eFfE8E766FE11",
"labelStore": "0x57C06ec4a92eC4eEB7c4eaFaBfBc64661483f3Bb",
"rootRegistry": "0x0dC5af261429d661BbF4903AbB79049a0713e04B",
"dosRegistry": "0x95366f1E44532F50c022aEefF708F424b7854173",
"reverseRegistry": "0x4d5429C2c0EE50b903407F44F8A21d20CAAcB475",
"priceOracle": "0x2eE958BcF29d140cdf64ad767f32E2554B0CCfa6",
"dosRegistrar": "0x4E5B48aC8B221aAF8cFF070671CB6Eeea2122b5c",
"permissionedResolverImplementation": "0x7C320eF16DE9CDD1BB9591AACF38c4c26f391e25",
"userRegistryImplementation": "0x440F74b09DfD3a848a9168d7d73784bCC03eF590",
"gatewayProvider": "0xEc7dFA7Dd3C7882d8716782689cE85e1Ccf9C68c",
"universalResolver": "0xbd0f96e3E8510a1bb806cAA9836fc76dfb1a5c47",
"reverseRegistrar": "0xD445f533aeC6E8BAE185f9903943e7B2E655472e",
"smokeResolver": "0xf2de697266073C4ba4D546d4305c228BBF5B1813"
}
}
83 changes: 83 additions & 0 deletions contracts/script/foundry/DeployDOSTestnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
pragma solidity >=0.8.20;

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

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

import {EACBaseRolesLib} from "~src/access-control/libraries/EACBaseRolesLib.sol";
import {
DEFAULT_ROLE_BITMAP,
StandardRentPriceOracle
} from "~src/registrar/StandardRentPriceOracle.sol";
import {PermissionedRegistry} from "~src/registry/PermissionedRegistry.sol";
import {IRegistry} from "~src/registry/interfaces/IRegistry.sol";
import {RegistryRolesLib} from "~src/registry/libraries/RegistryRolesLib.sol";
import {PermissionedResolver} from "~src/resolver/PermissionedResolver.sol";
import {PermissionedResolverLib} from "~src/resolver/libraries/PermissionedResolverLib.sol";
Expand All @@ -19,6 +22,20 @@ 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 {
uint256 internal constant EXPECTED_CHAIN_ID = 3939;
address internal constant EXPECTED_DEPLOYER = 0x99999e454138f6be73E2bE82c890bc5765749999;
address internal constant EXPECTED_OWNER = 0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD;
uint256 internal constant MIN_DEPLOYMENT_BALANCE = 1 ether;
string internal constant BENS_SMOKE_LABEL = "bens-smoke";
string internal constant BENS_SMOKE_NAME = "bens-smoke.dos";
uint64 internal constant BENS_SMOKE_LIFETIME = 10 * 365 days;

error UnexpectedChain(uint256 actual, uint256 expected);
error UnexpectedDeployer(address actual, address expected);
error UnexpectedOwner(address actual, address expected);
error UnexpectedBeneficiary(address actual, address expected);
error InsufficientDeploymentBalance(uint256 actual, uint256 required);

/// @notice Contracts produced by the DOS testnet deployment profile.
struct TestnetDeployment {
WrappedDOS wdos;
Expand All @@ -33,6 +50,7 @@ contract DeployDOSTestnet is DeployDOS {
address beneficiary = vm.envOr("BENEFICIARY", owner);
address broadcaster = vm.addr(privateKey);

preflight(broadcaster, owner, beneficiary);
vm.startBroadcast(privateKey);
TestnetDeployment memory testnetDeployment =
deployTestnet(broadcaster, owner, beneficiary, block.chainid);
Expand All @@ -41,6 +59,25 @@ contract DeployDOSTestnet is DeployDOS {
deployment = testnetDeployment.names;
}

/// @notice Fails before broadcasting if the Testnet deployment configuration is not canonical.
function preflight(address broadcaster, address owner, address beneficiary) public view {
if (block.chainid != EXPECTED_CHAIN_ID) {
revert UnexpectedChain(block.chainid, EXPECTED_CHAIN_ID);
}
if (broadcaster != EXPECTED_DEPLOYER) {
revert UnexpectedDeployer(broadcaster, EXPECTED_DEPLOYER);
}
if (owner != EXPECTED_OWNER) {
revert UnexpectedOwner(owner, EXPECTED_OWNER);
}
if (beneficiary != EXPECTED_OWNER) {
revert UnexpectedBeneficiary(beneficiary, EXPECTED_OWNER);
}
if (broadcaster.balance < MIN_DEPLOYMENT_BALANCE) {
revert InsufficientDeploymentBalance(broadcaster.balance, MIN_DEPLOYMENT_BALANCE);
}
}

/// @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.
Expand All @@ -55,9 +92,55 @@ contract DeployDOSTestnet is DeployDOS {
IERC20(address(deployment.wdos)),
chainId
);
_registerBensSmokeName(deployment.names, initialOwner, owner, chainId);
_handoff(deployment.names, initialOwner, owner);
}

/// @dev Creates one stable forward and reverse record used by Graph Node and BENS acceptance gates.
function _registerBensSmokeName(
Deployment memory deployment,
address initialOwner,
address owner,
uint256 chainId
)
internal
{
bytes[] memory setters = new bytes[](0);
PermissionedResolver resolver =
PermissionedResolver(
deployment.verifiableFactory.deployProxy(
address(deployment.permissionedResolverImplementation),
1,
abi.encodeCall(
PermissionedResolver.initialize,
(initialOwner, EACBaseRolesLib.ALL_ROLES, setters)
)
)
);
bytes32 node = NameCoder.namehash(NameCoder.encode(BENS_SMOKE_NAME), 0);

PermissionedRegistry registry = deployment.dosRegistry;
registry.grantRootRoles(RegistryRolesLib.ROLE_REGISTRAR, initialOwner);
registry.register(
BENS_SMOKE_LABEL,
owner,
IRegistry(address(0)),
address(resolver),
0,
uint64(block.timestamp + BENS_SMOKE_LIFETIME)
);
registry.revokeRootRoles(RegistryRolesLib.ROLE_REGISTRAR, initialOwner);

// Resolver records must be emitted after ResolverUpdated creates the
// dynamic Graph Node source. Dynamic sources cannot replay prior logs.
resolver.setAddr(node, initialOwner);
resolver.setAddr(node, (1 << 31) | chainId, abi.encodePacked(initialOwner));

deployment.reverseRegistrar.setName(BENS_SMOKE_NAME);
resolver.grantRootRoles(EACBaseRolesLib.ALL_ROLES, owner);
resolver.revokeRootRoles(EACBaseRolesLib.ALL_ROLES, initialOwner);
}

function _handoff(Deployment memory deployment, address initialOwner, address owner) internal {
if (owner == initialOwner) {
return;
Expand Down
115 changes: 115 additions & 0 deletions contracts/script/foundry/Invoke-DeployDOSTestnet.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
[CmdletBinding()]
param(
[string]$RpcUrl = "https://test.doschain.com",
[switch]$Broadcast
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$expectedRpcUrl = "https://test.doschain.com"
$expectedChainId = 3939
$expectedGenesisHash = "0x36f98b2e8b3084d57efc46622a065c2a96ed51aca86ac229bce998be6b8abd2c"
$expectedDeployer = "0x99999e454138f6be73e2be82c890bc5765749999"
$expectedOwner = "0x310bc061214ee89af5cfb28a6ebf96c5436fa3cd"
$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 Testnet"
}

$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 Testnet"
}

$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/DeployDOSTestnet.s.sol:DeployDOSTestnet" `
--rpc-url $RpcUrl `
--slow
if ($LASTEXITCODE -ne 0) {
throw "Foundry simulation failed"
}

if (-not $Broadcast) {
Write-Output "DOS_TESTNET_ENSV2_SIMULATION_OK"
return
}

& $forge script "script/foundry/DeployDOSTestnet.s.sol:DeployDOSTestnet" `
--rpc-url $RpcUrl `
--broadcast `
--slow
if ($LASTEXITCODE -ne 0) {
throw "Foundry broadcast failed"
}
Write-Output "DOS_TESTNET_ENSV2_BROADCAST_OK"
}
finally {
Pop-Location
}
Loading
Loading