diff --git a/contracts/confidential-wrapper/.env.example b/contracts/confidential-wrapper/.env.example index ab54f6e7..f2267e6f 100644 --- a/contracts/confidential-wrapper/.env.example +++ b/contracts/confidential-wrapper/.env.example @@ -1,9 +1,9 @@ -# Authentication (use one of these) +# Authentication MNEMONIC= PRIVATE_KEY= # RPC URLs -MAINNET_RPC_URL= +ETHEREUM_RPC_URL= POLYGON_RPC_URL= SEPOLIA_RPC_URL= AMOY_RPC_URL= @@ -49,8 +49,9 @@ CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_1=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC # ConfidentialWrapper upgrade # ---------------------------------------------------------------------------- -# ConfidentialWrapper upgrade version label used by upgrade tasks -CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL=v2 +# Optional unique wrapper identifier for task:deployConfidentialWrapperImpl +# CONFIDENTIAL_WRAPPER_UPGRADE_NAME= +CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG=v4 # Archive RPC used by the Foundry live mainnet-fork tests (make fork-test) ETHEREUM_MAINNET_FORK_RPC_URL=https://ethereum-rpc.publicnode.com/ diff --git a/contracts/confidential-wrapper/.gitignore b/contracts/confidential-wrapper/.gitignore index feb85893..801fab18 100644 --- a/contracts/confidential-wrapper/.gitignore +++ b/contracts/confidential-wrapper/.gitignore @@ -35,14 +35,15 @@ npm-debug.log* # Typechain typechain-types/ -# Hardhat -deployments/ +# Hardhat — live-network deployment state is committed; ignore local chains +deployments/localhost/ +deployments/hardhat/ -# OpenZeppelin -.openzeppelin/ +# OpenZeppelin — live-network manifests are committed; ignore local-chain manifests +.openzeppelin/unknown-*.json # Foundry broadcast/ # Fhevm -fhevmTemp/ \ No newline at end of file +fhevmTemp/ diff --git a/contracts/confidential-wrapper/README.md b/contracts/confidential-wrapper/README.md index e04407c5..99bfa8ab 100644 --- a/contracts/confidential-wrapper/README.md +++ b/contracts/confidential-wrapper/README.md @@ -14,11 +14,12 @@ Wraps standard ERC20 tokens into confidential ERC7984 tokens using FHE. Deployed | Variable | Description | | --- | --- | -| `MNEMONIC` or `PRIVATE_KEY` | Authentication for the deployer account | -| `MAINNET_RPC_URL` | RPC URL for mainnet | -| `POLYGON_RPC_URL` | RPC URL for Polygon mainnet | -| `SEPOLIA_RPC_URL` | RPC URL for Sepolia testnet | -| `ETHERSCAN_API_KEY` | Etherscan API key (required for contract verification) | +| `MNEMONIC` or `PRIVATE_KEY` | Local signer for the deployer account | +| `ETHEREUM_RPC_URL` | RPC URL for the `ethereum` network (mainnet) | +| `POLYGON_RPC_URL` | RPC URL for the `polygon` network (Polygon mainnet) | +| `SEPOLIA_RPC_URL` | RPC URL for the `sepolia` network (Sepolia testnet) | +| `AMOY_RPC_URL` | RPC URL for the `amoy` network (Polygon Amoy testnet) | +| `ETHERSCAN_API_KEY` | Etherscan API key (required for Etherscan verification; Blockscout/Sourcify need none) | ### Task inputs (batch deployment) @@ -30,16 +31,19 @@ Wraps standard ERC20 tokens into confidential ERC7984 tokens using FHE. Deployed | `CONFIDENTIAL_WRAPPER_CONTRACT_URI_{i}` | Contract URI metadata for the wrapper at index `i` | | `CONFIDENTIAL_WRAPPER_UNDERLYING_ADDRESS_{i}` | Address of the underlying ERC20 token for the wrapper at index `i` | | `CONFIDENTIAL_WRAPPER_OWNER_ADDRESS_{i}` | Owner address for the wrapper at index `i` | -| `CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_{i}` | Optional JSON array of observer addresses to seed during initialization | +| `CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_{i}` | JSON array of observer addresses to seed during initialization; use `[]` for none | | `CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_{i}` | Address allowed to call `pause()`, set during initialization; the zero address disables pausing | -### Task inputs (batch deploy upgrade implementations) +Every variable above is required in the batch path — a missing or misspelled one aborts the run +rather than deploying a wrapper with no observers or no pauser. Opt out explicitly with `[]` and the +zero address. + +### Task inputs (upgrade implementation) | Variable | Description | | --- | --- | -| `NUM_CONFIDENTIAL_WRAPPERS` | Same meaning as batch deployment: how many wrappers are listed in `.env` | -| `CONFIDENTIAL_WRAPPER_NAME_{i}` | Name of the wrapper at index `i` | -| `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL` | Version label appended to the saved implementation artifact (e.g. `v2`), shared for all wrappers in the batch upgrade/verify tasks | +| `CONFIDENTIAL_WRAPPER_UPGRADE_NAME` | Optional wrapper identifier included in the artifact (e.g. `cUSDT`). Used by `task:deployConfidentialWrapperImpl` when `--name` is omitted. Omit for a shared implementation | +| `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG` | Version tag appended to the saved implementation artifact (e.g. `v4`). Used by `task:deployConfidentialWrapperImpl` when `--version-tag` is omitted | > **Underlying deny-list configuration:** the selector alone carries enablement. > Consumers of `getUnderlyingDenyListSelector` determine enablement with `selector != 0`. A deny-list @@ -79,20 +83,20 @@ npx hardhat task:deployConfidentialWrapper \ --underlying-deny-list-selector 0x00000000 \ --initial-observers '[]' \ --pauser 0x2222222222222222222222222222222222222222 \ - --network testnet + --network sepolia ``` ### `task:deployAllConfidentialWrappers` Deploy all confidential wrapper contracts defined in the `.env` file. Reads `NUM_CONFIDENTIAL_WRAPPERS` and iterates over each wrapper's environment variables (`CONFIDENTIAL_WRAPPER_NAME_{i}`, `CONFIDENTIAL_WRAPPER_SYMBOL_{i}`, etc.). -Each wrapper must also provide the V3 initializer configuration: +Each wrapper must also provide the V3/V4 initializer configuration: | Variable | Description | | --- | --- | | `CONFIDENTIAL_WRAPPER_BLOCKED_USERS_{i}` | JSON array of addresses to seed into the wrapper denylist | | `CONFIDENTIAL_WRAPPER_UNDERLYING_DENY_LIST_SELECTOR_{i}` | Function selector used to query the underlying token denylist; `0x00000000` disables the check | -| `CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_{i}` | Optional JSON array of observer addresses to seed during initialization | +| `CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_{i}` | JSON array of observer addresses to seed during initialization; use `[]` for none | | `CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_{i}` | Address allowed to call `pause()`; the zero address disables pausing | **Parameters:** None (configuration is read from environment variables). @@ -100,7 +104,7 @@ Each wrapper must also provide the V3 initializer configuration: **Example:** ```bash -npx hardhat task:deployAllConfidentialWrappers --network testnet +npx hardhat task:deployAllConfidentialWrappers --network ``` ### `task:verifyConfidentialWrapper` @@ -118,7 +122,7 @@ Verify a single confidential wrapper contract (both proxy and implementation) on ```bash npx hardhat task:verifyConfidentialWrapper \ --proxy-address 0x1234567890123456789012345678901234567890 \ - --network testnet + --network ``` ### `task:verifyAllConfidentialWrappers` @@ -130,66 +134,56 @@ Verify all deployed confidential wrapper contracts on Etherscan. Reads wrapper n **Example:** ```bash -npx hardhat task:verifyAllConfidentialWrappers --network testnet +npx hardhat task:verifyAllConfidentialWrappers --network ``` -### `task:deployWrapperImplementation` +### `task:deployConfidentialWrapperImpl` Deploy a new `ConfidentialWrapper` implementation contract without upgrading any proxy. The proxy upgrade is handled separately by the DAO. +The artifact is `ConfidentialWrapper__Impl`, or `ConfidentialWrapper___Impl` when a name is provided. + **Parameters:** | Parameter | Type | Required | Description | | --- | --- | --- | --- | -| `--name` | `string` | Yes | The name of the wrapper this implementation is for | -| `--label` | `string` | Yes | A version label appended to the artifact name (e.g. `"v2"`) | +| `--name` | `string` | No | Wrapper identifier in the artifact (e.g. `"cUSDT"`). | +| `--version-tag` | `string` | No | Version tag appended to the saved artifact name (e.g. `"v4"`). Defaults to `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG` | **Example:** ```bash -npx hardhat task:deployWrapperImplementation --name "Confidential USDT" --label "v2" --network testnet +npx hardhat task:deployConfidentialWrapperImpl --name cUSDT --version-tag v4 --network ``` -### `task:deployAllWrapperImplementations` - -Requires that `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL` is set in the `.env` file. +Shared implementation (no per-wrapper name): -Deploy upgrade implementations for all wrappers defined in the `.env` file. Reads `NUM_CONFIDENTIAL_WRAPPERS`, `CONFIDENTIAL_WRAPPER_NAME_{i}`, and `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL`. - -**Parameters:** None (configuration is read from environment variables). +```bash +npx hardhat task:deployConfidentialWrapperImpl --version-tag v4 --network +``` -**Example:** +Or, with `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG` (and optionally `CONFIDENTIAL_WRAPPER_UPGRADE_NAME`) set in `.env`: ```bash -npx hardhat task:deployAllWrapperImplementations --network testnet +npx hardhat task:deployConfidentialWrapperImpl --network ``` -### `task:verifyWrapperImplementation` +### `task:verifyConfidentialWrapperImpl` -Verify a single `ConfidentialWrapper` implementation contract on Etherscan. +Verify a `ConfidentialWrapper` implementation contract on Etherscan. **Parameters:** | Parameter | Type | Required | Description | | --- | --- | --- | --- | -| `--address` | `string` | Yes | The address of the implementation contract to verify | - -**Example:** - -```bash -npx hardhat task:verifyWrapperImplementation --address 0x1234567890123456789012345678901234567890 --network testnet -``` - -### `task:verifyAllWrapperImplementations` - -Verify upgrade implementation contracts for all wrappers on Etherscan. Looks up deployment artifacts using `CONFIDENTIAL_WRAPPER_NAME_{i}` and `CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL`. - -**Parameters:** None (configuration is read from environment variables and deployment artifacts). +| `--impl-address` | `string` | Yes | The address of the implementation contract to verify | **Example:** ```bash -npx hardhat task:verifyAllWrapperImplementations --network testnet +npx hardhat task:verifyConfidentialWrapperImpl \ + --impl-address 0x1234567890123456789012345678901234567890 \ + --network ``` ## Scripts diff --git a/contracts/confidential-wrapper/deployments/sepolia/.chainId b/contracts/confidential-wrapper/deployments/sepolia/.chainId new file mode 100644 index 00000000..bd8d1cd4 --- /dev/null +++ b/contracts/confidential-wrapper/deployments/sepolia/.chainId @@ -0,0 +1 @@ +11155111 \ No newline at end of file diff --git a/contracts/confidential-wrapper/hardhat.config.ts b/contracts/confidential-wrapper/hardhat.config.ts index 11001fa5..6f9593a8 100644 --- a/contracts/confidential-wrapper/hardhat.config.ts +++ b/contracts/confidential-wrapper/hardhat.config.ts @@ -44,7 +44,7 @@ const accounts: HttpNetworkAccountsUserConfig | undefined = MNEMONIC if (accounts == null) { console.warn( - 'Could not find MNEMONIC or PRIVATE_KEY environment variables. It will not be possible to execute transactions in your example.', + 'No signer configured. Read-only tasks still work; to broadcast transactions, set MNEMONIC or PRIVATE_KEY.', ); } @@ -77,8 +77,8 @@ const config: HardhatUserConfig = { }, networks: { // ChainID must be specified in order to be able to verify contracts using the fhevm hardhat plugin - mainnet: { - url: process.env.MAINNET_RPC_URL || '', + ethereum: { + url: process.env.ETHEREUM_RPC_URL || '', accounts, chainId: 1, }, @@ -89,15 +89,14 @@ const config: HardhatUserConfig = { accounts, chainId: 137, }, - // ChainID must be specified in order to be able to verify contracts using the fhevm hardhat plugin - testnet: { + sepolia: { url: process.env.SEPOLIA_RPC_URL || '', accounts, chainId: 11155111, }, // FHEVM config for chainId 80002 comes from the locally vendored // contracts/fhevm/ZamaConfig.sol (aligned with @fhevm/solidity 0.13.2). - 'polygon-amoy': { + amoy: { url: process.env.AMOY_RPC_URL || '', accounts, chainId: 80002, @@ -128,6 +127,12 @@ const config: HardhatUserConfig = { etherscan: { apiKey: process.env.ETHERSCAN_API_KEY!, }, + sourcify: { + enabled: true, + }, + blockscout: { + enabled: true, + }, exposed: { imports: true, initializers: true, diff --git a/contracts/confidential-wrapper/package.json b/contracts/confidential-wrapper/package.json index 7e3d8937..2b4f1e11 100644 --- a/contracts/confidential-wrapper/package.json +++ b/contracts/confidential-wrapper/package.json @@ -68,7 +68,7 @@ "prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"", "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", "test": "hardhat test", - "test:sepolia": "hardhat test --network testnet" + "test:sepolia": "hardhat test --network sepolia" }, "overrides": { "@fhevm/solidity": "0.11.1", diff --git a/contracts/confidential-wrapper/tasks/deploy.ts b/contracts/confidential-wrapper/tasks/deploy.ts index 1dd0c052..7dc2c679 100644 --- a/contracts/confidential-wrapper/tasks/deploy.ts +++ b/contracts/confidential-wrapper/tasks/deploy.ts @@ -1,26 +1,40 @@ -import { getRequiredEnvVar } from './utils/loadVariables'; -import { ZeroAddress } from 'ethers'; +import { Signer, ZeroAddress } from 'ethers'; import { task, types } from 'hardhat/config'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { getRequiredEnvVar } from './utils/loadVariables'; export const CONTRACT_NAME = 'ConfidentialWrapper'; -// Get the deployment name for a confidential wrapper -export function getConfidentialWrapperName(tokenName: string): string { - return `ConfidentialWrapper_${tokenName}`; +// The deploy signer: local PRIVATE_KEY/MNEMONIC via namedAccounts (`deployer` = accounts[0]). +export async function getDeployerSigner(hre: HardhatRuntimeEnvironment): Promise { + const { deployer } = await hre.getNamedAccounts(); + if (!deployer) { + throw new Error('No signer configured: set PRIVATE_KEY or MNEMONIC'); + } + return hre.ethers.getSigner(deployer); +} + +// Artifact names are keyed by token symbol (e.g. `cUSDT`), not the human name, which can contain +// spaces/parens that make bad filenames (`ConfidentialWrapper_Confidential Token Test_Proxy.json`). +export function getConfidentialWrapperName(tokenSymbol: string): string { + return `ConfidentialWrapper_${tokenSymbol}`; +} + +export function getConfidentialWrapperImplName(tokenSymbol: string): string { + return `ConfidentialWrapper_${tokenSymbol}_Impl`; } -// Get the implementation deployment name for a confidential wrapper -export function getConfidentialWrapperImplName(tokenName: string): string { - return `ConfidentialWrapper_${tokenName}_Impl`; +// `name` is optional because one version usually ships a single shared implementation; it exists for +// the cases where a version needs more than one implementation live at once (e.g. treasury variants). +export function getConfidentialWrapperUpgradeImplName(versionTag: string, name?: string): string { + return name ? `ConfidentialWrapper_${name}_${versionTag}_Impl` : `ConfidentialWrapper_${versionTag}_Impl`; } -// Get the proxy deployment name for a confidential wrapper -export function getConfidentialWrapperProxyName(tokenName: string): string { - return `ConfidentialWrapper_${tokenName}_Proxy`; +export function getConfidentialWrapperProxyName(tokenSymbol: string): string { + return `ConfidentialWrapper_${tokenSymbol}_Proxy`; } -type ConfidentialWrapperInitConfig = { +export type ConfidentialWrapperInitConfig = { name: string; symbol: string; contractUri: string; @@ -36,11 +50,11 @@ function getRequiredJsonEnvVar(name: string): T { return JSON.parse(getRequiredEnvVar(name)) as T; } -// Deploy a confidential wrapper contract as a function async function deployConfidentialWrapper(initConfig: ConfidentialWrapperInitConfig, hre: HardhatRuntimeEnvironment) { - const { ethers, upgrades, deployments, getNamedAccounts } = hre; + const { ethers, upgrades, deployments } = hre; const { save, getArtifact } = deployments; - const { deployer } = await getNamedAccounts(); + const signer = await getDeployerSigner(hre); + const deployer = await signer.getAddress(); const { name, symbol, @@ -53,8 +67,8 @@ async function deployConfidentialWrapper(initConfig: ConfidentialWrapperInitConf pauser, } = initConfig; - // Deploy the proxy contract - const confidentialWrapperFactory = await ethers.getContractFactory(CONTRACT_NAME); + // Connecting the factory to `signer` routes both the impl and proxy deploy through it. + const confidentialWrapperFactory = await ethers.getContractFactory(CONTRACT_NAME, signer); const proxy = await upgrades.deployProxy( confidentialWrapperFactory, [name, symbol, contractUri, underlying, owner, blockedUsers, underlyingDenyListSelector, initialObservers, pauser], @@ -80,11 +94,12 @@ async function deployConfidentialWrapper(initConfig: ConfidentialWrapperInitConf ].join('\n'), ); - // Save the deployment artifacts const implementationAddress = await upgrades.erc1967.getImplementationAddress(proxyAddress); const artifact = await getArtifact(CONTRACT_NAME); - await save(getConfidentialWrapperProxyName(name), { address: proxyAddress, abi: artifact.abi }); - await save(getConfidentialWrapperImplName(name), { address: implementationAddress, abi: artifact.abi }); + await save(getConfidentialWrapperProxyName(symbol), { address: proxyAddress, abi: artifact.abi }); + await save(getConfidentialWrapperImplName(symbol), { address: implementationAddress, abi: artifact.abi }); + + return proxyAddress; } // Deploy a confidential wrapper contract @@ -97,8 +112,9 @@ async function deployConfidentialWrapper(initConfig: ConfidentialWrapperInitConf // --owner "0x1234567890123456789012345678901234567890" \ // --blocked-users '["0x1111111111111111111111111111111111111111"]' \ // --underlying-deny-list-selector "0xfe575a87" \ +// --initial-observers '[]' \ // --pauser "0x2222222222222222222222222222222222222222" \ -// --network testnet +// --network sepolia task('task:deployConfidentialWrapper') .addParam('name', 'The name of the confidential wrapper contract to deploy', undefined, types.string) .addParam('symbol', 'The symbol of the confidential wrapper contract to deploy', undefined, types.string) @@ -143,7 +159,8 @@ task('task:deployConfidentialWrapper') }, hre, ) { - await deployConfidentialWrapper( + // Return the proxy address so callers can surface it without reconstructing the artifact name. + return deployConfidentialWrapper( { name, symbol, @@ -161,15 +178,13 @@ task('task:deployConfidentialWrapper') // Deploy all confidential wrapper contracts // Example usage: -// npx hardhat task:deployAllConfidentialWrappers --network testnet +// npx hardhat task:deployAllConfidentialWrappers --network sepolia task('task:deployAllConfidentialWrappers').setAction(async function (_, hre) { console.log('Deploying confidential wrapper contracts...'); - // Get the number of confidential wrappers from environment variable const numWrappers = parseInt(getRequiredEnvVar('NUM_CONFIDENTIAL_WRAPPERS')); for (let i = 0; i < numWrappers; i++) { - // Get the name from environment variable const name = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_NAME_${i}`); const symbol = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_SYMBOL_${i}`); const contractUri = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_CONTRACT_URI_${i}`); @@ -177,6 +192,9 @@ task('task:deployAllConfidentialWrappers').setAction(async function (_, hre) { const owner = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_OWNER_ADDRESS_${i}`); const blockedUsers = getRequiredJsonEnvVar(`CONFIDENTIAL_WRAPPER_BLOCKED_USERS_${i}`); const underlyingDenyListSelector = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_UNDERLYING_DENY_LIST_SELECTOR_${i}`); + // Every field is required in the batch path: a forgotten or typo'd var must abort the run + // rather than silently deploy with no observers / no pauser. Use `'[]'` / the zero address to + // opt out explicitly. const initialObservers = getRequiredJsonEnvVar(`CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_${i}`); const pauser = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_${i}`); @@ -196,16 +214,30 @@ task('task:deployAllConfidentialWrappers').setAction(async function (_, hre) { console.log('✅ All confidential wrapper contracts deployed\n'); }); +function resolveOptionalTaskInput(cliValue: unknown, envName: string): string | undefined { + if (typeof cliValue === 'string' && cliValue.trim() !== '') return cliValue.trim(); + const fromEnv = process.env[envName]?.trim(); + return fromEnv || undefined; +} + +function assertArtifactSegment(value: string, field: string): string { + if (!/^[A-Za-z0-9._-]+$/.test(value)) { + throw new Error(`${field} must be a filesystem-safe identifier (e.g. cUSDT or v4), not "${value}"`); + } + return value; +} + // Deploy a bare ConfidentialWrapper implementation (no proxy). -// Used when preparing an upgrade proposal: deploy the new implementation, then call -// `upgradeToAndCall(implAddress, reinitializeVX_calldata)` on the existing proxy. -// Example usage: -// npx hardhat task:deployConfidentialWrapperImpl --network testnet -async function deployConfidentialWrapperImpl(hre: HardhatRuntimeEnvironment) { - const { getNamedAccounts, ethers, deployments, network } = hre; +async function deployConfidentialWrapperImpl( + versionTag: string, + name: string | undefined, + hre: HardhatRuntimeEnvironment, +) { + const { ethers, deployments, network } = hre; const { save, getArtifact } = deployments; - const { deployer } = await getNamedAccounts(); - const deployerSigner = await ethers.getSigner(deployer); + const deployerSigner = await getDeployerSigner(hre); + const deployer = await deployerSigner.getAddress(); + const artifactName = getConfidentialWrapperUpgradeImplName(versionTag, name); const factory = await ethers.getContractFactory(CONTRACT_NAME, deployerSigner); const implementation = await factory.deploy(); @@ -217,6 +249,7 @@ async function deployConfidentialWrapperImpl(hre: HardhatRuntimeEnvironment) { [ `✅ Deployed ${CONTRACT_NAME} implementation:`, ` - Implementation address: ${implementationAddress}`, + ` - Artifact: ${artifactName}`, ` - Deployed by deployer account: ${deployer}`, ` - Network: ${network.name}`, '', @@ -224,20 +257,33 @@ async function deployConfidentialWrapperImpl(hre: HardhatRuntimeEnvironment) { ); const artifact = await getArtifact(CONTRACT_NAME); - await save(`${CONTRACT_NAME}_Impl`, { address: implementationAddress, abi: artifact.abi }); + await save(artifactName, { address: implementationAddress, abi: artifact.abi }); return implementationAddress; } -task('task:deployConfidentialWrapperImpl').setAction(async function (_, hre) { - console.log(`Deploying ${CONTRACT_NAME} implementation...\n`); - await deployConfidentialWrapperImpl(hre); -}); - -task('task:verifyConfidentialWrapperImpl') - .addParam('implAddress', 'The address of the implementation contract to verify', '', types.string) - .setAction(async function ({ implAddress }, hre) { - const { run } = hre; - console.log(`Verifying ${CONTRACT_NAME} implementation at ${implAddress}...\n`); - await run('verify:verify', { address: implAddress, constructorArguments: [] }); +task('task:deployConfidentialWrapperImpl') + .addOptionalParam( + 'name', + 'Wrapper identifier included in the saved implementation artifact (e.g. "cUSDT"). Defaults to CONFIDENTIAL_WRAPPER_UPGRADE_NAME', + undefined, + types.string, + ) + .addOptionalParam( + 'versionTag', + 'Version tag appended to the saved implementation artifact (e.g. "v4"). Defaults to CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG', + undefined, + types.string, + ) + .setAction(async function ({ name, versionTag }, hre) { + const resolvedVersionTag = resolveOptionalTaskInput(versionTag, 'CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG'); + if (!resolvedVersionTag) { + throw new Error('Provide --version-tag or set CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_TAG'); + } + const resolvedName = resolveOptionalTaskInput(name, 'CONFIDENTIAL_WRAPPER_UPGRADE_NAME'); + const safeVersionTag = assertArtifactSegment(resolvedVersionTag, 'versionTag'); + const safeName = resolvedName ? assertArtifactSegment(resolvedName, 'name') : undefined; + const tag = safeName ? `${safeName} ${safeVersionTag}` : safeVersionTag; + console.log(`Deploying ${CONTRACT_NAME} implementation (${tag})...\n`); + await deployConfidentialWrapperImpl(safeVersionTag, safeName, hre); }); diff --git a/contracts/confidential-wrapper/tasks/verify.ts b/contracts/confidential-wrapper/tasks/verify.ts index 61f0cdfb..e1a9b41f 100644 --- a/contracts/confidential-wrapper/tasks/verify.ts +++ b/contracts/confidential-wrapper/tasks/verify.ts @@ -1,28 +1,112 @@ -import { getConfidentialWrapperProxyName } from './deploy'; +import { CONTRACT_NAME, getConfidentialWrapperProxyName } from './deploy'; import { getRequiredEnvVar } from './utils/loadVariables'; import { task, types } from 'hardhat/config'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; + +const ALREADY_VERIFIED = /already verified/i; + +/** + * Whether an explorer failure is nothing but "this is already verified". + */ +function isAlreadyVerified(err: unknown): boolean { + const message = err instanceof Error ? err.message : String(err); + const entries = message.split(/\n\n(?:Error|Warning) \d+: /).slice(1); + if (entries.length === 0) { + return ALREADY_VERIFIED.test(message); + } + return entries.every(entry => ALREADY_VERIFIED.test(entry)); +} + +/** + * Verify on every explorer enabled in hardhat.config. + * + * - Etherscan: required (OZ hardhat-upgrades intercepts `verify:etherscan` for proxies). + * - Blockscout / Sourcify: best-effort — failures must not fail the task after Etherscan has + * already succeeded. Pass `bestEffort: false` to skip them for an address they cannot verify. + */ +async function verifyOnEnabledExplorers( + hre: HardhatRuntimeEnvironment, + address: string, + constructorArguments: unknown[] = [], + { bestEffort = true }: { bestEffort?: boolean } = {}, +): Promise { + const { run, config } = hre; + + if (config.etherscan.enabled !== false) { + try { + // Prefer verify:etherscan so OZ's proxy interceptor runs; verify:verify would also kick off + // Sourcify in-process and could throw after a successful Etherscan verify. + await run('verify:etherscan', { + address, + constructorArgsParams: constructorArguments, + }); + } catch (err) { + if (!isAlreadyVerified(err)) throw err; + console.log(`Already verified on Etherscan: ${address}`); + } + } + + if (!bestEffort) return; + + if (config.blockscout?.enabled) { + try { + await run('verify:blockscout', { address }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (isAlreadyVerified(err)) { + console.log(`Already verified on Blockscout: ${address}`); + } else { + console.warn(`Blockscout verification failed for ${address} (best-effort):\n${msg}`); + } + } + } + + if (config.sourcify?.enabled) { + try { + await run('verify:sourcify', { address }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (isAlreadyVerified(err)) { + console.log(`Already verified on Sourcify: ${address}`); + } else { + console.warn(`Sourcify verification failed for ${address} (best-effort):\n${msg}`); + } + } + } +} // Verify a confidential wrapper contract // Example usage: -// npx hardhat task:verifyConfidentialWrapper --proxy-address 0x1234567890123456789012345678901234567890 --network testnet +// npx hardhat task:verifyConfidentialWrapper --proxy-address 0x1234567890123456789012345678901234567890 --network sepolia task('task:verifyConfidentialWrapper') .addParam('proxyAddress', 'The address of the confidential wrapper proxy contract to verify', '', types.string) .setAction(async function ({ proxyAddress }, hre) { - const { upgrades, run } = hre; + const { upgrades } = hre; const implementationAddress = await upgrades.erc1967.getImplementationAddress(proxyAddress); + // Etherscan only for the proxy: it is OZ's precompiled ERC1967Proxy, not an artifact of this + // project, and it takes two constructor arguments. Blockscout and Sourcify would therefore warn + // on every single run, training operators to ignore the warnings that do matter on the + // implementation. Etherscan still gets the call because that is what links proxy to + // implementation in its UI. console.log(`Verifying confidential wrapper proxy contract at ${proxyAddress}...\n`); - await run('verify:verify', { - address: proxyAddress, - constructorArguments: [], - }); + await verifyOnEnabledExplorers(hre, proxyAddress, [], { bestEffort: false }); console.log(`Verifying confidential wrapper implementation contract at ${implementationAddress}...\n`); - await run('verify:verify', { - address: implementationAddress, - constructorArguments: [], - }); + await verifyOnEnabledExplorers(hre, implementationAddress, []); + }); + +// Verify a bare ConfidentialWrapper implementation (no proxy). +// Example usage: +// npx hardhat task:verifyConfidentialWrapperImpl \ +// --impl-address 0x1234567890123456789012345678901234567890 \ +// --network sepolia +task('task:verifyConfidentialWrapperImpl') + .addParam('implAddress', 'The address of the implementation contract to verify', '', types.string) + .setAction(async function ({ implAddress }, hre) { + console.log(`Verifying ${CONTRACT_NAME} implementation at ${implAddress}...\n`); + await verifyOnEnabledExplorers(hre, implAddress, []); }); // Verify all confidential wrapper contracts @@ -30,23 +114,18 @@ task('task:verifyConfidentialWrapper') // verify one of them. However, since they are proxied, verifying all of them has the benefit of linking // the proxies with their implementation on Etherscan. // Example usage: -// npx hardhat task:verifyAllConfidentialWrappers --network testnet +// npx hardhat task:verifyAllConfidentialWrappers --network sepolia task('task:verifyAllConfidentialWrappers').setAction(async function (_, hre) { const { run, deployments } = hre; const { get } = deployments; - // Get the number of confidential wrappers from environment variable const numWrappers = parseInt(getRequiredEnvVar('NUM_CONFIDENTIAL_WRAPPERS')); for (let i = 0; i < numWrappers; i++) { - // Get the name from environment variable - const name = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_NAME_${i}`); + const symbol = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_SYMBOL_${i}`); try { - // Get the proxy address from deployments - const proxyAddress = await get(getConfidentialWrapperProxyName(name)); - - // Verify the confidential wrapper contract + const proxyAddress = await get(getConfidentialWrapperProxyName(symbol)); await run('task:verifyConfidentialWrapper', { proxyAddress: proxyAddress.address }); } catch (error) { console.error('An error occurred:', error); diff --git a/contracts/confidential-wrapper/test/tasks/deploy.ts b/contracts/confidential-wrapper/test/tasks/deploy.ts index 2ad1996d..499a52d9 100644 --- a/contracts/confidential-wrapper/test/tasks/deploy.ts +++ b/contracts/confidential-wrapper/test/tasks/deploy.ts @@ -2,6 +2,7 @@ import { getConfidentialWrapperName, getConfidentialWrapperProxyName, getConfidentialWrapperImplName, + getConfidentialWrapperUpgradeImplName, CONTRACT_NAME, } from '../../tasks/deploy'; import { getRequiredEnvVar } from '../../tasks/utils/loadVariables'; @@ -37,10 +38,11 @@ describe('ConfidentialWrapper Deployment', function () { const owner = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_OWNER_ADDRESS_${i}`); const blockedUsers = getRequiredJsonEnvVar(`CONFIDENTIAL_WRAPPER_BLOCKED_USERS_${i}`); const underlyingDenyListSelector = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_UNDERLYING_DENY_LIST_SELECTOR_${i}`); + const initialObservers = getRequiredJsonEnvVar(`CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_${i}`); const pauser = getRequiredEnvVar(`CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_${i}`); - // Get the deployed proxy contract - const proxyDeployment = await hre.deployments.get(getConfidentialWrapperProxyName(name)); + // Get the deployed proxy contract (artifacts are keyed by symbol) + const proxyDeployment = await hre.deployments.get(getConfidentialWrapperProxyName(symbol)); const confidentialWrapper = await hre.ethers.getContractAt(CONTRACT_NAME, proxyDeployment.address); // Verify the contract was deployed @@ -56,6 +58,7 @@ describe('ConfidentialWrapper Deployment', function () { expect(await confidentialWrapper.isBlockedOnWrapper(blockedUserAddress)).to.equal(true); } expect(await confidentialWrapper.getUnderlyingDenyListSelector()).to.equal(underlyingDenyListSelector); + expect(await confidentialWrapper.observers()).to.deep.equal(initialObservers); expect(await confidentialWrapper.pauser()).to.equal(pauser); } }); @@ -73,5 +76,10 @@ describe('ConfidentialWrapper Deployment', function () { it('Should generate correct ConfidentialWrapper implementation name', function () { expect(getConfidentialWrapperImplName('MyToken')).to.equal('ConfidentialWrapper_MyToken_Impl'); }); + + it('Should generate correct upgrade implementation artifact name', function () { + expect(getConfidentialWrapperUpgradeImplName('v4')).to.equal('ConfidentialWrapper_v4_Impl'); + expect(getConfidentialWrapperUpgradeImplName('v4', 'cUSDT')).to.equal('ConfidentialWrapper_cUSDT_v4_Impl'); + }); }); }); diff --git a/contracts/confidential-wrapper/test/tasks/upgrades/wrapperUpgradeChain.test.ts b/contracts/confidential-wrapper/test/tasks/upgrades/wrapperUpgradeChain.test.ts index ebe5e84e..6f9fe5f1 100644 --- a/contracts/confidential-wrapper/test/tasks/upgrades/wrapperUpgradeChain.test.ts +++ b/contracts/confidential-wrapper/test/tasks/upgrades/wrapperUpgradeChain.test.ts @@ -1,5 +1,5 @@ import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; -import { CONTRACT_NAME } from '../../../tasks/deploy'; +import { CONTRACT_NAME, getConfidentialWrapperUpgradeImplName } from '../../../tasks/deploy'; import { expect } from 'chai'; import { ethers as ethersUtils } from 'ethers'; import hre from 'hardhat'; @@ -62,8 +62,8 @@ describe('ConfidentialWrapper Upgrade Chain', function () { } async function deployCurrentImplementation() { - await hre.run('task:deployConfidentialWrapperImpl'); - const implDeployment = await hre.deployments.get(`${CONTRACT_NAME}_Impl`); + await hre.run('task:deployConfidentialWrapperImpl', { versionTag: 'v4' }); + const implDeployment = await hre.deployments.get(getConfidentialWrapperUpgradeImplName('v4')); return implDeployment.address; } diff --git a/docs/deployment/deploy-wrapper-runbook.md b/docs/deployment/deploy-wrapper-runbook.md index a5222618..2218b31e 100644 --- a/docs/deployment/deploy-wrapper-runbook.md +++ b/docs/deployment/deploy-wrapper-runbook.md @@ -17,7 +17,7 @@ Before starting, collect the following for each wrapper being deployed: | Initial observers list (JSON array) | Addresses authorized to decrypt confidential amounts on behalf of the wrapper. Use `'[]'` if none. See the observer scope warning below | | Pauser address | Address allowed to call `pause()`. Use the zero address to deploy with pausing disabled | | Contract URI JSON metadata | Follow the pattern `data:application/json;utf8,{"name":"...","symbol":"...","description":"..."}` | -| `MNEMONIC` or `PRIVATE_KEY` for the deployer | DFNS / internal secrets | +| `MNEMONIC` or `PRIVATE_KEY` for the deployer | Self provided | | `ETHERSCAN_API_KEY` | Etherscan dashboard | | RPC URL for the target network | Infura / Alchemy / internal node / public endpoint | @@ -85,10 +85,10 @@ Populate `.env` with all required values. For a batch of `N` wrappers (replace ` # Auth MNEMONIC= # or PRIVATE_KEY= # Set the RPC URL for your target network: -# MAINNET_RPC_URL= # --network mainnet -# SEPOLIA_RPC_URL= # --network testnet +# ETHEREUM_RPC_URL= # --network ethereum +# SEPOLIA_RPC_URL= # --network sepolia # POLYGON_RPC_URL= # --network polygon -# AMOY_RPC_URL= # --network polygon-amoy +# AMOY_RPC_URL= # --network amoy ETHERSCAN_API_KEY= NUM_CONFIDENTIAL_WRAPPERS=N @@ -129,7 +129,7 @@ CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_{i}= # address allowed to call pause **Batch (recommended when deploying multiple wrappers):** ```bash -npx hardhat task:deployAllConfidentialWrappers --network mainnet +npx hardhat task:deployAllConfidentialWrappers --network ethereum ``` **Single wrapper:** @@ -147,7 +147,7 @@ npx hardhat task:deployConfidentialWrapper \ --underlying-deny-list-selector 0x59bf1abe \ --initial-observers '[]' \ --pauser 0x0000000000000000000000000000000000000000 \ - --network mainnet + --network ethereum ``` On success, each wrapper prints: @@ -167,7 +167,7 @@ Record the proxy address for every wrapper. **Batch:** ```bash -npx hardhat task:verifyAllConfidentialWrappers --network mainnet +npx hardhat task:verifyAllConfidentialWrappers --network ethereum ``` **Single:** @@ -175,7 +175,7 @@ npx hardhat task:verifyAllConfidentialWrappers --network mainnet ```bash npx hardhat task:verifyConfidentialWrapper \ --proxy-address \ - --network mainnet + --network ethereum ``` This verifies both the proxy contract and the implementation contract. Since all wrappers share the same implementation bytecode, the implementation source will already be verified from the second wrapper onward. Etherscan will report a duplicate-verification notice, which is expected. @@ -215,7 +215,7 @@ Before deploying, confirm whether a matching implementation for this version alr - Existing wrapper deployments may have the implementation that you need already - `.openzeppelin/.json` for an entry matching the current source. -- `deployments//` for prior `ConfidentialWrapper_