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
136 changes: 88 additions & 48 deletions contracts/script/foundry/DeployDOSDomainPolicyTestnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {PermissionedRegistry} from "~src/registry/PermissionedRegistry.sol";

/// @title Deploy DOS ID Domain Policy on DOS Testnet
/// @notice Replaces the public Testnet registrar role with a policy-controlled registrar.
/// @dev Required env: PRIVATE_KEY (the canonical registry owner) and DOS_DOMAIN_VOUCHER_SIGNER.
/// @dev Required env: PRIVATE_KEY (the canonical registry owner), DOS_DOMAIN_VOUCHER_SIGNER,
/// DOS_DOMAIN_REGISTRY, DOS_DOMAIN_PRICE_ORACLE, and DOS_DOMAIN_LEGACY_REGISTRAR.
/// No private voucher key is read by this deployment script.
contract DeployDOSDomainPolicyTestnet is Script {
////////////////////////////////////////////////////////////////////////
Expand All @@ -24,6 +25,16 @@ contract DeployDOSDomainPolicyTestnet is Script {
DOSPolicyRegistrar registrar;
}

/// @notice Existing DOS Name contracts and policy configuration for this deployment.
struct DeploymentConfig {
PermissionedRegistry registry;
StandardRentPriceOracle priceOracle;
address legacyRegistrar;
address voucherSigner;
uint32 minimumScore;
address defaultResolver;
}

////////////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////////////
Expand All @@ -34,15 +45,6 @@ contract DeployDOSDomainPolicyTestnet is Script {
/// @dev Account that owns the deployed registry roles.
address internal constant EXPECTED_OWNER = 0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD;

/// @dev Existing `.dos` registry address.
address internal constant DOS_REGISTRY = 0x95366f1E44532F50c022aEefF708F424b7854173;

/// @dev Existing `.dos` rent price oracle address.
address internal constant PRICE_ORACLE = 0x2eE958BcF29d140cdf64ad767f32E2554B0CCfa6;

/// @dev Existing public registrar whose roles are retired by this script.
address internal constant LEGACY_DOS_REGISTRAR = 0x4E5B48aC8B221aAF8cFF070671CB6Eeea2122b5c;

/// @dev Renewable period after a name expires.
uint64 internal constant GRACE_PERIOD = 28 days;

Expand All @@ -59,6 +61,13 @@ contract DeployDOSDomainPolicyTestnet is Script {
uint256 internal constant REGISTRAR_ROLES =
RegistryRolesLib.ROLE_REGISTRAR | RegistryRolesLib.ROLE_RENEW;

/// @dev Root admin roles required to move the registrar permissions.
uint256 internal constant REGISTRAR_ADMIN_ROLES =
RegistryRolesLib.ROLE_REGISTRAR_ADMIN | RegistryRolesLib.ROLE_RENEW_ADMIN;

/// @dev Minimum native DOS balance required before broadcasting policy deployment.
uint256 internal constant MIN_DEPLOYMENT_BALANCE = 1 ether;

////////////////////////////////////////////////////////////////////////
// Errors
////////////////////////////////////////////////////////////////////////
Expand All @@ -73,72 +82,103 @@ contract DeployDOSDomainPolicyTestnet is Script {
error LegacyRegistrarAlreadyRetired(address registrar);
/// @dev Error selector: `0x16319300`
error InvalidMinimumScore(uint256 value);
error MissingRegistrarAdmin(address owner);
error InsufficientDeploymentBalance(uint256 actual, uint256 required);

////////////////////////////////////////////////////////////////////////
// Implementation
////////////////////////////////////////////////////////////////////////

/// @notice Deploys the policy and registrar, then atomically moves registrar roles per transaction.
/// @notice Deploys the policy and registrar, then grants and revokes registrar roles in separate transactions.
/// @dev Policy subsidy funding is intentionally separate: seed it only after address verification.
/// @return deployment The deployed policy and policy-controlled registrar.
function run() external returns (Deployment memory deployment) {
uint256 privateKey = vm.envUint("PRIVATE_KEY");
address voucherSigner = vm.envAddress("DOS_DOMAIN_VOUCHER_SIGNER");
DeploymentConfig memory config = deploymentConfig();
preflight(vm.addr(privateKey), config);

vm.startBroadcast(privateKey);
deployment.policy = deployPolicy(config);
deployment.registrar = deployRegistrar(config, deployment.policy);
config.registry.grantRootRoles(REGISTRAR_ROLES, address(deployment.registrar));
config.registry.revokeRootRoles(REGISTRAR_ROLES, config.legacyRegistrar);
deployment.policy.setRegistrar(deployment.registrar);
vm.stopBroadcast();
}

/// @notice Reads and validates the runtime-specific contract configuration.
function deploymentConfig() internal view returns (DeploymentConfig memory config) {
uint256 configuredMinimumScore = vm.envOr("DOS_DOMAIN_MINIMUM_SCORE", uint256(20));
address defaultResolver = vm.envOr("DOS_DOMAIN_DEFAULT_RESOLVER", address(0));
if (configuredMinimumScore > type(uint32).max) {
revert InvalidMinimumScore(configuredMinimumScore);
}

address broadcaster = vm.addr(privateKey);
preflight(broadcaster);
config = DeploymentConfig({registry: PermissionedRegistry(
vm.envAddress("DOS_DOMAIN_REGISTRY")
), priceOracle: StandardRentPriceOracle(vm.envAddress("DOS_DOMAIN_PRICE_ORACLE")), legacyRegistrar: vm.envAddress(
"DOS_DOMAIN_LEGACY_REGISTRAR"
), voucherSigner: vm.envAddress("DOS_DOMAIN_VOUCHER_SIGNER"), minimumScore: uint32(
configuredMinimumScore
), defaultResolver: vm.envOr("DOS_DOMAIN_DEFAULT_RESOLVER", address(0))});
}

PermissionedRegistry registry = PermissionedRegistry(DOS_REGISTRY);
vm.startBroadcast(privateKey);
deployment.policy = new DosDomainPolicy(
registry,
EXPECTED_OWNER,
voucherSigner,
uint32(configuredMinimumScore),
defaultResolver
);
deployment.registrar = new DOSPolicyRegistrar(
EXPECTED_OWNER,
registry,
EXPECTED_OWNER,
StandardRentPriceOracle(PRICE_ORACLE),
GRACE_PERIOD,
MIN_COMMITMENT_AGE,
MAX_COMMITMENT_AGE,
MIN_REGISTER_DURATION,
address(deployment.policy)
);
registry.revokeRootRoles(REGISTRAR_ROLES, LEGACY_DOS_REGISTRAR);
registry.grantRootRoles(REGISTRAR_ROLES, address(deployment.registrar));
deployment.policy.setRegistrar(deployment.registrar);
vm.stopBroadcast();
/// @notice Deploys the score-gated policy contract.
function deployPolicy(DeploymentConfig memory config) internal returns (DosDomainPolicy) {
return
new DosDomainPolicy(
config.registry,
EXPECTED_OWNER,
config.voucherSigner,
config.minimumScore,
config.defaultResolver
);
}

/// @notice Deploys the registrar that enforces the policy contract.
function deployRegistrar(DeploymentConfig memory config, DosDomainPolicy policy)
internal
returns (DOSPolicyRegistrar)
{
return
new DOSPolicyRegistrar(
EXPECTED_OWNER,
config.registry,
EXPECTED_OWNER,
config.priceOracle,
GRACE_PERIOD,
MIN_COMMITMENT_AGE,
MAX_COMMITMENT_AGE,
MIN_REGISTER_DURATION,
address(policy)
);
}

/// @notice Validates network, broadcaster, existing contracts, and legacy registrar roles before deployment.
/// @param broadcaster The address derived from the transaction private key.
function preflight(address broadcaster) public view {
function preflight(address broadcaster, DeploymentConfig memory config) public view {
if (block.chainid != EXPECTED_CHAIN_ID) {
revert UnexpectedChain(block.chainid, EXPECTED_CHAIN_ID);
}
if (broadcaster != EXPECTED_OWNER) {
revert UnexpectedOwner(broadcaster, EXPECTED_OWNER);
}
if (DOS_REGISTRY.code.length == 0) {
revert MissingContractCode(DOS_REGISTRY);
if (broadcaster.balance < MIN_DEPLOYMENT_BALANCE) {
revert InsufficientDeploymentBalance(broadcaster.balance, MIN_DEPLOYMENT_BALANCE);
}
if (address(config.registry).code.length == 0) {
revert MissingContractCode(address(config.registry));
}
if (address(config.priceOracle).code.length == 0) {
revert MissingContractCode(address(config.priceOracle));
}
if (PRICE_ORACLE.code.length == 0) {
revert MissingContractCode(PRICE_ORACLE);
if (config.legacyRegistrar.code.length == 0) {
revert MissingContractCode(config.legacyRegistrar);
}
if (LEGACY_DOS_REGISTRAR.code.length == 0) {
revert MissingContractCode(LEGACY_DOS_REGISTRAR);
if (!config.registry.hasRootRoles(REGISTRAR_ROLES, config.legacyRegistrar)) {
revert LegacyRegistrarAlreadyRetired(config.legacyRegistrar);
}
if (!PermissionedRegistry(DOS_REGISTRY).hasRootRoles(REGISTRAR_ROLES, LEGACY_DOS_REGISTRAR)) {
revert LegacyRegistrarAlreadyRetired(LEGACY_DOS_REGISTRAR);
if (!config.registry.hasRootRoles(REGISTRAR_ADMIN_ROLES, broadcaster)) {
revert MissingRegistrarAdmin(broadcaster);
}
}
}
2 changes: 1 addition & 1 deletion contracts/script/foundry/DeployDOSTestnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {LibLabel} from "~src/utils/LibLabel.sol";
/// @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_DEPLOYER = 0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD;
address internal constant EXPECTED_OWNER = 0x310Bc061214ee89aF5CfB28a6ebF96c5436fa3CD;
uint256 internal constant MIN_DEPLOYMENT_BALANCE = 1 ether;
string internal constant BENS_SMOKE_LABEL = "bens-smoke";
Expand Down
107 changes: 107 additions & 0 deletions contracts/script/foundry/Invoke-DeployDOSDomainPolicyTestnet.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
[CmdletBinding()]
param(
[string]$RpcUrl = "https://test.doschain.com",
[switch]$Broadcast
)

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

$expectedRpcUrl = "https://test.doschain.com"
$expectedChainId = 3939
$expectedGenesisHash = "0x1f6dd88694681d79a3a56313f59de56b1b6555c8e06a2ef377084f1e897feef4"
$expectedOwner = "0x310bc061214ee89af5cfb28a6ebf96c5436fa3cd"
$minimumBalanceWei = [System.Numerics.BigInteger]::Parse("1000000000000000000")
$requiredEnvironmentVariables = @(
"PRIVATE_KEY",
"DOS_DOMAIN_VOUCHER_SIGNER",
"DOS_DOMAIN_REGISTRY",
"DOS_DOMAIN_PRICE_ORACLE",
"DOS_DOMAIN_LEGACY_REGISTRAR"
)

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

foreach ($name in $requiredEnvironmentVariables) {
$environmentItem = Get-Item -Path "Env:$name" -ErrorAction SilentlyContinue
if ($null -eq $environmentItem -or [string]::IsNullOrWhiteSpace($environmentItem.Value)) {
throw "$name 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

$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 $expectedOwner --rpc-url $RpcUrl
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($balanceOutput)) {
throw "Unable to read the canonical owner balance"
}
$balance = [System.Numerics.BigInteger]::Parse($balanceOutput.Trim())
if ($balance -lt $minimumBalanceWei) {
throw "Canonical owner balance is below the 1 DOS deployment floor"
}

$contractsRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
Push-Location $contractsRoot
try {
& $forge script "script/foundry/DeployDOSDomainPolicyTestnet.s.sol:DeployDOSDomainPolicyTestnet" `
--rpc-url $RpcUrl `
--slow
if ($LASTEXITCODE -ne 0) {
throw "Foundry simulation failed"
}

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

& $forge script "script/foundry/DeployDOSDomainPolicyTestnet.s.sol:DeployDOSDomainPolicyTestnet" `
--rpc-url $RpcUrl `
--broadcast `
--slow
if ($LASTEXITCODE -ne 0) {
throw "Foundry broadcast failed"
}
Write-Output "DOS_TESTNET_DOMAIN_POLICY_BROADCAST_OK"
}
finally {
Pop-Location
}
4 changes: 2 additions & 2 deletions contracts/script/foundry/Invoke-DeployDOSTestnet.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ $ErrorActionPreference = "Stop"

$expectedRpcUrl = "https://test.doschain.com"
$expectedChainId = 3939
$expectedGenesisHash = "0x36f98b2e8b3084d57efc46622a065c2a96ed51aca86ac229bce998be6b8abd2c"
$expectedDeployer = "0x99999e454138f6be73e2be82c890bc5765749999"
$expectedGenesisHash = "0x1f6dd88694681d79a3a56313f59de56b1b6555c8e06a2ef377084f1e897feef4"
$expectedDeployer = "0x310bc061214ee89af5cfb28a6ebf96c5436fa3cd"
$expectedOwner = "0x310bc061214ee89af5cfb28a6ebf96c5436fa3cd"
$minimumBalanceWei = [System.Numerics.BigInteger]::Parse("1000000000000000000")

Expand Down
3 changes: 2 additions & 1 deletion contracts/test/e2e/devnet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ describe("Devnet", () => {
const t = await env.sync();
const block1 = await env.client.getBlock();
expect(block1.timestamp).toBeGreaterThanOrEqual(block0.timestamp);
expectVar({ t }).toStrictEqual(block1.timestamp);
expectVar({ t }).toBeGreaterThanOrEqual(block0.timestamp);
expectVar({ t }).toBeLessThanOrEqual(block1.timestamp);
});

it("warp", async () => {
Expand Down
Loading
Loading