feat: prepare ENSv2 mainnet deployment - #4
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the mainnet deployment configuration and scripts for the DOS Name Service, including a Solidity deployment script, a PowerShell automation script, and comprehensive unit tests. It also updates the subgraph mapping logic to align with the canonical ENS model for wrapped names, ensuring Domain.owner points to the registry contract while tracking the token holder via wrappedOwner. The review feedback focuses on improving the robustness of the PowerShell script (Invoke-DeployDOSMainnet.ps1) by ensuring normalized environment variables are correctly propagated to Foundry and adding proper error handling/null-checks for external CLI commands before invoking string methods.
| $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 |
There was a problem hiding this comment.
The environment variable $env:OWNER is not updated with the trimmed and normalized $owner address. If the user provides OWNER with leading/trailing whitespace or mixed casing, the PowerShell validation will pass, but the raw, uncleaned value will be passed to the Foundry script. This can cause vm.envAddress("OWNER") to fail or revert during execution.
Additionally, we can simplify the default assignment of $beneficiary by using the already cleaned $owner variable instead of $env:OWNER.
$owner = $env:OWNER.Trim().ToLowerInvariant()
if ($owner -ne $expectedOwner) {
throw "OWNER does not match the canonical DOS Names owner"
}
$env:OWNER = $owner
$beneficiary = if ([string]::IsNullOrWhiteSpace($env:BENEFICIARY)) {
$owner
} else {
$env:BENEFICIARY
}
$beneficiary = $beneficiary.Trim().ToLowerInvariant()
if ($beneficiary -ne $expectedOwner) {
throw "BENEFICIARY does not match the canonical DOS Names beneficiary"
}
$env:BENEFICIARY = $beneficiary
| $derivedDeployer = (& $cast wallet address --private-key $privateKey).Trim().ToLowerInvariant() | ||
| if ($LASTEXITCODE -ne 0 -or $derivedDeployer -ne $expectedDeployer) { | ||
| throw "PRIVATE_KEY does not match the canonical DOS Names deployer" | ||
| } | ||
|
|
||
| $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" | ||
| } |
There was a problem hiding this comment.
Several native command calls (such as cast wallet address, cast chain-id, cast block 0, cast code, and cast call) are executed in pipelines or subexpressions with immediate method calls (like .Trim(), .ToLowerInvariant(), or piping to ConvertFrom-Json) without checking $LASTEXITCODE or verifying if the output is null/empty first.
If any of these RPC or CLI commands fail (e.g., due to network timeouts, invalid keys, or RPC errors), the command will return null/empty, and calling methods on them will throw a generic and unhelpful PowerShell error (e.g., You cannot call a method on a null-valued expression or JSON parsing errors).
It is much more robust to capture the command output first, verify $LASTEXITCODE and that the output is not empty, and then perform the string operations or JSON parsing. This ensures clear, actionable error messages are presented to the operator during mainnet deployment.
$derivedDeployerOutput = & $cast wallet address --private-key $privateKey
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($derivedDeployerOutput)) {
throw "Failed to derive deployer address from PRIVATE_KEY"
}
$derivedDeployer = $derivedDeployerOutput.Trim().ToLowerInvariant()
if ($derivedDeployer -ne $expectedDeployer) {
throw "PRIVATE_KEY does not match the canonical DOS Names deployer"
}
$chainIdOutput = & $cast chain-id --rpc-url $RpcUrl
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($chainIdOutput)) {
throw "Failed to retrieve chain ID from RPC"
}
$chainId = [int]($chainIdOutput.Trim())
if ($chainId -ne $expectedChainId) {
throw "RPC chain ID does not match DOS Mainnet"
}
$blockJson = & $cast block 0 --rpc-url $RpcUrl --json
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($blockJson)) {
throw "Failed to retrieve genesis block from RPC"
}
$genesis = (ConvertFrom-Json $blockJson).hash
if ([string]::IsNullOrWhiteSpace($genesis)) {
throw "Genesis block hash is missing"
}
$genesis = $genesis.ToLowerInvariant()
if ($genesis -ne $expectedGenesisHash) {
throw "RPC genesis hash does not match DOS Mainnet"
}
$paymentTokenCodeOutput = & $cast code $expectedPaymentToken --rpc-url $RpcUrl
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($paymentTokenCodeOutput)) {
throw "Failed to retrieve bytecode for payment token"
}
$paymentTokenCode = $paymentTokenCodeOutput.Trim()
if ($paymentTokenCode -eq "0x" -or $paymentTokenCode -eq "0x0") {
throw "Canonical WDOS has no Mainnet bytecode"
}
$decimalsOutput = & $cast call $expectedPaymentToken "decimals()(uint8)" --rpc-url $RpcUrl
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($decimalsOutput)) {
throw "Failed to retrieve decimals for payment token"
}
$paymentTokenDecimals = [int]($decimalsOutput.Trim())
if ($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"
}
Summary
Validation
No Mainnet transaction is broadcast by this PR.