From 1d3463b0971f3bc28b47730069bf77e54f651815 Mon Sep 17 00:00:00 2001 From: Seth Schmidt Date: Wed, 5 Aug 2026 17:09:04 -0400 Subject: [PATCH 01/11] feat(confidential-wrapper): add params-driven deploy pipeline Introduce deploy-params// as the reviewed source of deploy intent, rename Hardhat networks to ethereum/sepolia, and pass V4 initialObservers through deploy tasks so initialize arity matches main. --- contracts/confidential-wrapper/.env.example | 5 +- contracts/confidential-wrapper/.gitignore | 11 +- contracts/confidential-wrapper/README.md | 30 ++-- .../deploy-params/README.md | 19 +++ .../deploy-params/SCHEMA.md | 53 +++++++ .../mainnet/ethereum/network.json | 6 + .../mainnet/ethereum/wrappers.json | 47 ++++++ .../testnet/sepolia/network.json | 6 + .../testnet/sepolia/wrappers.json | 55 +++++++ .../deployments/sepolia/.chainId | 1 + .../confidential-wrapper/hardhat.config.ts | 63 ++++++-- .../confidential-wrapper/tasks/deploy.ts | 97 ++++++++----- .../tasks/utils/deployParams.ts | 134 ++++++++++++++++++ .../confidential-wrapper/tasks/verify.ts | 86 ++++++++--- .../confidential-wrapper/test/tasks/deploy.ts | 6 +- docs/deployment/deploy-wrapper-runbook.md | 14 +- 16 files changed, 538 insertions(+), 95 deletions(-) create mode 100644 contracts/confidential-wrapper/deploy-params/README.md create mode 100644 contracts/confidential-wrapper/deploy-params/SCHEMA.md create mode 100644 contracts/confidential-wrapper/deploy-params/mainnet/ethereum/network.json create mode 100644 contracts/confidential-wrapper/deploy-params/mainnet/ethereum/wrappers.json create mode 100644 contracts/confidential-wrapper/deploy-params/testnet/sepolia/network.json create mode 100644 contracts/confidential-wrapper/deploy-params/testnet/sepolia/wrappers.json create mode 100644 contracts/confidential-wrapper/deployments/sepolia/.chainId create mode 100644 contracts/confidential-wrapper/tasks/utils/deployParams.ts diff --git a/contracts/confidential-wrapper/.env.example b/contracts/confidential-wrapper/.env.example index ab54f6e7..5b0d552d 100644 --- a/contracts/confidential-wrapper/.env.example +++ b/contracts/confidential-wrapper/.env.example @@ -1,12 +1,11 @@ -# 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= # API Keys ETHERSCAN_API_KEY= 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..90044d45 100644 --- a/contracts/confidential-wrapper/README.md +++ b/contracts/confidential-wrapper/README.md @@ -14,11 +14,11 @@ 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) | +| `ETHERSCAN_API_KEY` | Etherscan API key (required for Etherscan verification; Blockscout/Sourcify need none) | ### Task inputs (batch deployment) @@ -79,7 +79,7 @@ npx hardhat task:deployConfidentialWrapper \ --underlying-deny-list-selector 0x00000000 \ --initial-observers '[]' \ --pauser 0x2222222222222222222222222222222222222222 \ - --network testnet + --network sepolia ``` ### `task:deployAllConfidentialWrappers` @@ -100,7 +100,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 +118,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,7 +130,7 @@ 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` @@ -147,7 +147,7 @@ Deploy a new `ConfidentialWrapper` implementation contract without upgrading any **Example:** ```bash -npx hardhat task:deployWrapperImplementation --name "Confidential USDT" --label "v2" --network testnet +npx hardhat task:deployWrapperImplementation --name "Confidential USDT" --label "v2" --network ``` ### `task:deployAllWrapperImplementations` @@ -161,7 +161,7 @@ Deploy upgrade implementations for all wrappers defined in the `.env` file. Read **Example:** ```bash -npx hardhat task:deployAllWrapperImplementations --network testnet +npx hardhat task:deployAllWrapperImplementations --network ``` ### `task:verifyWrapperImplementation` @@ -177,7 +177,7 @@ Verify a single `ConfidentialWrapper` implementation contract on Etherscan. **Example:** ```bash -npx hardhat task:verifyWrapperImplementation --address 0x1234567890123456789012345678901234567890 --network testnet +npx hardhat task:verifyWrapperImplementation --address 0x1234567890123456789012345678901234567890 --network ``` ### `task:verifyAllWrapperImplementations` @@ -189,9 +189,13 @@ Verify upgrade implementation contracts for all wrappers on Etherscan. Looks up **Example:** ```bash -npx hardhat task:verifyAllWrapperImplementations --network testnet +npx hardhat task:verifyAllWrapperImplementations --network ``` +## Params-driven deployment + +Live deployments run from a private workflow that reads the reviewed [`deploy-params/`](./deploy-params/) files and invokes the Hardhat tasks documented above. To request a deployment, open a PR that adds or edits a params entry — see [`deploy-params/README.md`](./deploy-params/README.md). + ## Scripts ### Foundry mainnet-fork tests diff --git a/contracts/confidential-wrapper/deploy-params/README.md b/contracts/confidential-wrapper/deploy-params/README.md new file mode 100644 index 00000000..597b8301 --- /dev/null +++ b/contracts/confidential-wrapper/deploy-params/README.md @@ -0,0 +1,19 @@ +# Submitting deploy params + +## What + +`deploy-params/` holds the reviewed inputs for every ConfidentialWrapper +deployment: one entry per wrapper, under `//wrappers.json`. Field-by-field +reference: [`SCHEMA.md`](./SCHEMA.md). + +## How + +1. **Add your entry.** Edit `//wrappers.json` (e.g. `mainnet/ethereum/wrappers.json`), + keyed by the wrapper symbol. Each `underlying` may appear only once per network. +2. **Open a PR** against `main` with just that change. In the description, state the token being + wrapped and the source for the denylist selector and any blocked users. +3. **Get it reviewed and merged.** Merging says these values are cleared to deploy. +4. **Zama dispatches the deploy.** The workflow runs against the merged params. You do not need + access to it. +5. **Results land back here.** A follow-up PR adds the deployment artifacts and addresses. Registry + registration is a separate Protocol DAO governance action. diff --git a/contracts/confidential-wrapper/deploy-params/SCHEMA.md b/contracts/confidential-wrapper/deploy-params/SCHEMA.md new file mode 100644 index 00000000..5bb3bf1e --- /dev/null +++ b/contracts/confidential-wrapper/deploy-params/SCHEMA.md @@ -0,0 +1,53 @@ +# Deploy params schema + +Reviewed, source-of-truth inputs for the `contracts-confidential-wrapper-deploy` +workflow. How to submit a change: [README.md](./README.md). + +## Layout + +`//`, where **tier** = `testnet` | `mainnet` and **network** = the +Hardhat network name / chain (`sepolia`, `ethereum`, …). The directory is the source +of truth for the tier↔network mapping. + +``` +deploy-params/ +├── testnet/sepolia/{network,wrappers}.json # network.json: chainId, DAO, registry, … +└── mainnet/ethereum/{network,wrappers}.json +``` + +## `network.json` + +| Field | Meaning | +| ----------------------- | ------------------------------------------------------------------ | +| `chainId` | Expected chain id; preflight fails on mismatch | +| `dao` | Protocol DAO — the default wrapper `owner` | +| `registry` | `ConfidentialTokenWrappersRegistry` address | +| `minDeployerBalanceWei` | Preflight min deployer balance (string wei) | + +## `wrappers.json` + +`{ wrapperSymbol: entry }` — keyed by the wrapper symbol (e.g. `cUSDT`); the wrapped token is the `underlying` field (no separate `symbol` field). + +The `name` and `contractUri` are derived following existing conventions, and `owner` is set to the `dao` entry in `network.json`. + +The following fields are required: + +| Field | Type | Notes | +| ---------------------------- | --------- | ----------------------------------------------------------------------------------------- | +| `underlying` | address | The ERC-20 being wrapped (looked up by the deploy dispatch); each underlying appears once | +| `blockedUsers` | address[] | Seeded into the denylist; `[]` if none | +| `underlyingDenyListSelector` | bytes4 | `0x00000000` disables the check; a non-zero selector enables it | +| `initialObservers` | address[] | Seeded V4 observers; `[]` if none | + +Minimal entry: + +```json +{ + "cUSDT": { + "underlying": "0x9b5Cd13b8eFbB58Dc25A05CF411D8056058aDFfF", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000", + "initialObservers": [] + } +} +``` diff --git a/contracts/confidential-wrapper/deploy-params/mainnet/ethereum/network.json b/contracts/confidential-wrapper/deploy-params/mainnet/ethereum/network.json new file mode 100644 index 00000000..3947e011 --- /dev/null +++ b/contracts/confidential-wrapper/deploy-params/mainnet/ethereum/network.json @@ -0,0 +1,6 @@ +{ + "chainId": 1, + "dao": "0xB6D69D5F334d8B97B194617B53c6aB62f8681Ef3", + "registry": "0xeb5015fF021DB115aCe010f23F55C2591059bBA0", + "minDeployerBalanceWei": "150000000000000000" +} diff --git a/contracts/confidential-wrapper/deploy-params/mainnet/ethereum/wrappers.json b/contracts/confidential-wrapper/deploy-params/mainnet/ethereum/wrappers.json new file mode 100644 index 00000000..19eb455a --- /dev/null +++ b/contracts/confidential-wrapper/deploy-params/mainnet/ethereum/wrappers.json @@ -0,0 +1,47 @@ +{ + "cUSDC": { + "underlying": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "blockedUsers": [], + "underlyingDenyListSelector": "0xfe575a87" + }, + "cUSDT": { + "underlying": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "blockedUsers": [], + "underlyingDenyListSelector": "0x59bf1abe" + }, + "cWETH": { + "underlying": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cBRON": { + "underlying": "0xBA2C598E11eD093079cC324FCa5BbbA99F616E83", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cZAMA": { + "underlying": "0xA12CC123ba206d4031D1c7f6223D1C2Ec249f4f3", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "ctGBP": { + "underlying": "0x27f6c8289550fCE67f6B50BeD1F519966aFE5287", + "blockedUsers": [], + "underlyingDenyListSelector": "0x97f735d5" + }, + "cXAUt": { + "underlying": "0x68749665FF8D2d112Fa859AA293F07A622782F38", + "blockedUsers": [], + "underlyingDenyListSelector": "0xfbac3951" + }, + "cbbqTGBP": { + "underlying": "0xbeeffABcd0dB09589Dd21854aa760C52aB4bf04F", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "csteakcUSDC": { + "underlying": "0xbEEF00A59B577423653A1526c7009bdE103F542B", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + } +} diff --git a/contracts/confidential-wrapper/deploy-params/testnet/sepolia/network.json b/contracts/confidential-wrapper/deploy-params/testnet/sepolia/network.json new file mode 100644 index 00000000..f053751c --- /dev/null +++ b/contracts/confidential-wrapper/deploy-params/testnet/sepolia/network.json @@ -0,0 +1,6 @@ +{ + "chainId": 11155111, + "dao": "0x08e8a84c3c8c7cba165B1adcf67Ae4639eF84f52", + "registry": "0x2f0750Bbb0A246059d80e94c454586a7F27a128e", + "minDeployerBalanceWei": "20000000000000000" +} diff --git a/contracts/confidential-wrapper/deploy-params/testnet/sepolia/wrappers.json b/contracts/confidential-wrapper/deploy-params/testnet/sepolia/wrappers.json new file mode 100644 index 00000000..ab4f1a10 --- /dev/null +++ b/contracts/confidential-wrapper/deploy-params/testnet/sepolia/wrappers.json @@ -0,0 +1,55 @@ +{ + "cUSDCMock": { + "underlying": "0x9b5Cd13b8eFbB58Dc25A05CF411D8056058aDFfF", + "name": "Confidential USDC (Mock)", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cUSDTMock": { + "underlying": "0xa7dA08FafDC9097Cc0E7D4f113A61e31d7e8e9b0", + "name": "Confidential USDT (Mock)", + "contractUri": "data:application/json;utf8,{\"name\":\"Confidential USDT (Mock)\",\"symbol\":\"cUSDTMock\",\"description\":\"Confidential wrapper of USDT (Mock) shielding it into a confidential token\"}", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cWETHMock": { + "underlying": "0xff54739b16576FA5402F211D0b938469Ab9A5f3F", + "name": "Confidential WETH (Mock)", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cBRONMock": { + "underlying": "0xFf021fB13cA64e5354c62c954b949a88cfDEb25E", + "name": "Confidential BRON (Mock)", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cZAMAMock": { + "underlying": "0x75355a85c6FB9df5f0C80FF54e8747EEe9a0BF57", + "name": "Confidential ZAMA (Mock)", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "ctGBPMock": { + "underlying": "0x93c931278A2aad1916783F952f94276eA5111442", + "name": "Confidential tGBP (Mock)", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "cXAUtMock": { + "underlying": "0x24377AE4AA0C45ecEe71225007f17c5D423dd940", + "name": "Confidential XAUt (Mock)", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + }, + "ctGBP": { + "underlying": "0xf6Ef9ADB61A48E29E36bc873070A46A3D2667ff3", + "blockedUsers": [], + "underlyingDenyListSelector": "0x97f735d5" + }, + "cmTEST": { + "underlying": "0x10513c06D40D1D2Ce410570DfADA40D5b3968B3b", + "blockedUsers": [], + "underlyingDenyListSelector": "0x00000000" + } +} 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..e2aa53a4 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.', ); } @@ -66,39 +66,68 @@ subtask(TASK_TEST_GET_TEST_FILES).setAction(async (args, _hre, runSuper) => { const config: HardhatUserConfig = { solidity: { - version: '0.8.27', - settings: { - optimizer: { - enabled: true, - runs: 800, + // Hardhat picks the highest compiler matching a pragma, so listing 0.8.29 for + // hardhat-verify (OZ's precompiled ERC1967Proxy is 0.8.29) would also bump our + // ^0.8.27 impls. Pin ConfidentialWrapper via overrides; keep 0.8.29 in compilers + // so verify can match the proxy bytecode without CompilerVersionsMismatchError. + compilers: [ + { + version: '0.8.27', + settings: { + optimizer: { + enabled: true, + runs: 800, + }, + evmVersion: 'cancun', + }, + }, + { + version: '0.8.29', + settings: { + optimizer: { + enabled: true, + runs: 800, + }, + evmVersion: 'cancun', + }, + }, + ], + overrides: { + 'contracts/ConfidentialWrapper.sol': { + version: '0.8.27', + settings: { + optimizer: { + enabled: true, + runs: 800, + }, + evmVersion: 'cancun', + }, }, - evmVersion: 'cancun', }, }, 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.DEPLOYMENT_RPC_URL || process.env.ETHEREUM_RPC_URL || '', accounts, chainId: 1, }, // FHEVM config for chainId 137 comes from the locally vendored // contracts/fhevm/ZamaConfig.sol (aligned with @fhevm/solidity 0.13.2). polygon: { - url: process.env.POLYGON_RPC_URL || '', + url: process.env.DEPLOYMENT_RPC_URL || process.env.POLYGON_RPC_URL || '', accounts, chainId: 137, }, - // ChainID must be specified in order to be able to verify contracts using the fhevm hardhat plugin - testnet: { - url: process.env.SEPOLIA_RPC_URL || '', + sepolia: { + url: process.env.DEPLOYMENT_RPC_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': { - url: process.env.AMOY_RPC_URL || '', + url: process.env.DEPLOYMENT_RPC_URL || process.env.AMOY_RPC_URL || '', accounts, chainId: 80002, }, @@ -128,6 +157,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/tasks/deploy.ts b/contracts/confidential-wrapper/tasks/deploy.ts index 1dd0c052..31a6bdf9 100644 --- a/contracts/confidential-wrapper/tasks/deploy.ts +++ b/contracts/confidential-wrapper/tasks/deploy.ts @@ -1,26 +1,56 @@ -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}`; +type HreWithDeployHook = HardhatRuntimeEnvironment & { + zamaWrapperDeploy?: { + getDeployerSigner?: (hre: HardhatRuntimeEnvironment) => Promise; + resolveDeployerAddress?: (hre: HardhatRuntimeEnvironment) => Promise; + }; +}; + +// Select the deploy signer. Default: local PRIVATE_KEY/MNEMONIC via namedAccounts. +export async function getDeployerSigner(hre: HardhatRuntimeEnvironment): Promise { + const override = (hre as HreWithDeployHook).zamaWrapperDeploy?.getDeployerSigner; + if (override) { + return override(hre); + } + const { deployer } = await hre.getNamedAccounts(); + return hre.ethers.getSigner(deployer); } -// Get the implementation deployment name for a confidential wrapper -export function getConfidentialWrapperImplName(tokenName: string): string { - return `ConfidentialWrapper_${tokenName}_Impl`; +// Resolve the deployer address +export async function resolveDeployerAddress(hre: HardhatRuntimeEnvironment): Promise { + const override = (hre as HreWithDeployHook).zamaWrapperDeploy?.resolveDeployerAddress; + if (override) { + return override(hre); + } + // The exact account getDeployerSigner signs with (named `deployer` = accounts[0]). + const { deployer } = await hre.getNamedAccounts(); + if (!deployer) { + throw new Error('No signer configured: set PRIVATE_KEY or MNEMONIC'); + } + return deployer; } -// Get the proxy deployment name for a confidential wrapper -export function getConfidentialWrapperProxyName(tokenName: string): string { - return `ConfidentialWrapper_${tokenName}_Proxy`; +// 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}`; } -type ConfidentialWrapperInitConfig = { +export function getConfidentialWrapperImplName(tokenSymbol: string): string { + return `ConfidentialWrapper_${tokenSymbol}_Impl`; +} + +export function getConfidentialWrapperProxyName(tokenSymbol: string): string { + return `ConfidentialWrapper_${tokenSymbol}_Proxy`; +} + +export type ConfidentialWrapperInitConfig = { name: string; symbol: string; contractUri: string; @@ -36,11 +66,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 +83,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 +110,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 +128,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 +175,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 +194,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 +208,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 +230,13 @@ task('task:deployAllConfidentialWrappers').setAction(async function (_, hre) { console.log('✅ All confidential wrapper contracts deployed\n'); }); -// 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 +// Deploy a bare ConfidentialWrapper implementation (no proxy), for an upgrade proposal: deploy it, +// then call `upgradeToAndCall(implAddress, reinitializeVX_calldata)` on the existing proxy. async function deployConfidentialWrapperImpl(hre: HardhatRuntimeEnvironment) { - const { getNamedAccounts, ethers, deployments, network } = hre; + 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 factory = await ethers.getContractFactory(CONTRACT_NAME, deployerSigner); const implementation = await factory.deploy(); diff --git a/contracts/confidential-wrapper/tasks/utils/deployParams.ts b/contracts/confidential-wrapper/tasks/utils/deployParams.ts new file mode 100644 index 00000000..8dd9f180 --- /dev/null +++ b/contracts/confidential-wrapper/tasks/utils/deployParams.ts @@ -0,0 +1,134 @@ +/** + * Locate and read committed deploy-params, organized as `//{network,wrappers}.json` + * (tier = testnet | mainnet, network = the Hardhat network / chain). The directory layout is the + * source of truth for the tier↔network mapping, so the tier is never stored inside the files. + * + * Pure fs/path (no hardhat/ethers deps) so it can be imported without an import cycle. + */ +import { existsSync, readFileSync, readdirSync } from 'fs'; +import { join, resolve } from 'path'; + +const DEPLOY_PARAMS_ROOT = resolve(__dirname, '../../deploy-params'); + +export type NetworkConfig = { + chainId: number; + dao: string; + registry: string; + minDeployerBalanceWei: string; +}; + +/** Minimal shape needed to look up a wrappers.json entry by underlying address. */ +export type WrapperParamsEntry = { + underlying: string; + name?: string; + contractUri?: string; + owner?: string; + blockedUsers?: unknown; + underlyingDenyListSelector?: string; + initialObservers?: unknown; +}; + +export function readJsonFile(path: string): T { + if (!existsSync(path)) { + throw new Error(`File not found: ${path}`); + } + try { + return JSON.parse(readFileSync(path, 'utf8')) as T; + } catch (err) { + throw new Error(`Failed to parse JSON at ${path}: ${(err as Error).message}`); + } +} + +// Resolve a network's tier + directory by scanning tier dirs for `//network.json`. +// Network names must be unique across tiers (a chain belongs to exactly one tier). +export function resolveNetworkDir(networkName: string): { tier: string; dir: string } { + const tiers = readdirSync(DEPLOY_PARAMS_ROOT, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name); + + const matches = tiers.filter(tier => existsSync(join(DEPLOY_PARAMS_ROOT, tier, networkName, 'network.json'))); + + if (matches.length === 0) { + const available = tiers + .flatMap(tier => + readdirSync(join(DEPLOY_PARAMS_ROOT, tier), { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => `${tier}/${d.name}`), + ) + .sort(); + throw new Error( + `No deploy-params for network "${networkName}" under any tier in deploy-params/ ` + + `(available: ${available.join(', ') || ''})`, + ); + } + if (matches.length > 1) { + throw new Error( + `Network "${networkName}" is defined under multiple tiers (${matches.join(', ')}); ` + + `network names must be unique across tiers`, + ); + } + return { tier: matches[0], dir: join(DEPLOY_PARAMS_ROOT, matches[0], networkName) }; +} + +// Absolute paths to a network's params files, plus its resolved tier. +export function networkParamsPaths(networkName: string): { + tier: string; + dir: string; + networkJson: string; + wrappersJson: string; +} { + const { tier, dir } = resolveNetworkDir(networkName); + return { tier, dir, networkJson: join(dir, 'network.json'), wrappersJson: join(dir, 'wrappers.json') }; +} + +export function loadNetworkConfig(networkName: string): NetworkConfig { + return readJsonFile(networkParamsPaths(networkName).networkJson); +} + +/** + * Find the wrappers.json entry whose `underlying` matches (checksum-insensitive). + * Entries are keyed by wrapper symbol; each underlying may appear once. + */ +export function findWrapperByUnderlying( + networkName: string, + underlying: string, +): { symbol: string; entry: WrapperParamsEntry; paramsFile: string } { + const { tier, wrappersJson } = networkParamsPaths(networkName); + const paramsFile = `deploy-params/${tier}/${networkName}/wrappers.json`; + + if (!existsSync(wrappersJson)) { + throw new Error(`${paramsFile} not found — add it and merge before deploying`); + } + + const entries = readJsonFile>(wrappersJson); + const target = underlying.toLowerCase(); + const matches: { symbol: string; entry: WrapperParamsEntry }[] = []; + + for (const [symbol, entry] of Object.entries(entries)) { + if (!entry || typeof entry.underlying !== 'string') { + throw new Error(`Entry "${symbol}" in ${paramsFile} is missing an "underlying" address`); + } + if (entry.underlying.toLowerCase() === target) { + matches.push({ symbol, entry }); + } + } + + if (matches.length === 0) { + throw new Error( + `${paramsFile} has no entry with underlying ${underlying} — add it and merge before deploying ` + + `(have: ${Object.keys(entries).join(', ') || ''})`, + ); + } + if (matches.length > 1) { + throw new Error( + `Multiple entries in ${paramsFile} share underlying ${underlying} (${matches.map(m => m.symbol).join(', ')}); ` + + `each underlying may appear once`, + ); + } + + const { symbol, entry } = matches[0]; + if (symbol.length === 0) { + throw new Error(`Empty wrapper symbol key in ${paramsFile}`); + } + return { symbol, entry, paramsFile }; +} diff --git a/contracts/confidential-wrapper/tasks/verify.ts b/contracts/confidential-wrapper/tasks/verify.ts index 61f0cdfb..9e0adefe 100644 --- a/contracts/confidential-wrapper/tasks/verify.ts +++ b/contracts/confidential-wrapper/tasks/verify.ts @@ -1,28 +1,83 @@ import { getConfidentialWrapperProxyName } from './deploy'; import { getRequiredEnvVar } from './utils/loadVariables'; import { task, types } from 'hardhat/config'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; + +function isAlreadyVerified(err: unknown): boolean { + return /already verified/i.test(err instanceof Error ? err.message : String(err)); +} + +/** + * Verify on every explorer enabled in hardhat.config. + * + * - Etherscan: required (OZ hardhat-upgrades intercepts `verify:etherscan` for proxies). + * - Blockscout / Sourcify: best-effort — proxy bytecode is OZ's precompiled 0.8.29 artifact, so + * those providers often cannot match this repo's 0.8.27 compile; failures must not fail the task + * after Etherscan succeeds. (`verify:verify` also skips Blockscout entirely.) + */ +async function verifyOnEnabledExplorers( + hre: HardhatRuntimeEnvironment, + address: string, + constructorArguments: unknown[] = [], +): 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 (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); console.log(`Verifying confidential wrapper proxy contract at ${proxyAddress}...\n`); - await run('verify:verify', { - address: proxyAddress, - constructorArguments: [], - }); + await verifyOnEnabledExplorers(hre, proxyAddress, []); console.log(`Verifying confidential wrapper implementation contract at ${implementationAddress}...\n`); - await run('verify:verify', { - address: implementationAddress, - constructorArguments: [], - }); + await verifyOnEnabledExplorers(hre, implementationAddress, []); }); // Verify all confidential wrapper contracts @@ -30,23 +85,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..06d13736 100644 --- a/contracts/confidential-wrapper/test/tasks/deploy.ts +++ b/contracts/confidential-wrapper/test/tasks/deploy.ts @@ -37,10 +37,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 +57,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); } }); diff --git a/docs/deployment/deploy-wrapper-runbook.md b/docs/deployment/deploy-wrapper-runbook.md index a5222618..a9d9ccf3 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 | @@ -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:** @@ -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. @@ -241,14 +241,14 @@ Minimal `.env` required for this step: ```dotenv MNEMONIC= # or PRIVATE_KEY= -MAINNET_RPC_URL= +ETHEREUM_RPC_URL= ETHERSCAN_API_KEY= ``` Deploy the implementation contract: ```bash -npx hardhat task:deployConfidentialWrapperImpl --network mainnet +npx hardhat task:deployConfidentialWrapperImpl --network ethereum ``` The implementation is saved as `ConfidentialWrapper_Impl` in the deployments artifacts. Record the implementation address printed on success. @@ -258,7 +258,7 @@ The implementation is saved as `ConfidentialWrapper_Impl` in the deployments art ```bash npx hardhat task:verifyConfidentialWrapperImpl \ --impl-address \ - --network mainnet + --network ethereum ``` ### Step 5 — Submit the DAO upgrade proposal From ae15eba9022fd9f6ab9e96f0ce21b88e09be8807 Mon Sep 17 00:00:00 2001 From: Seth Schmidt Date: Tue, 18 Aug 2026 14:51:07 -0400 Subject: [PATCH 02/11] feat(confidential-wrapper): add --label to impl deploy task --- contracts/confidential-wrapper/.env.example | 7 --- contracts/confidential-wrapper/README.md | 53 ++++--------------- .../confidential-wrapper/tasks/deploy.ts | 28 +++++++--- .../confidential-wrapper/test/tasks/deploy.ts | 5 ++ .../upgrades/wrapperUpgradeChain.test.ts | 6 +-- docs/deployment/deploy-wrapper-runbook.md | 4 +- 6 files changed, 42 insertions(+), 61 deletions(-) diff --git a/contracts/confidential-wrapper/.env.example b/contracts/confidential-wrapper/.env.example index 5b0d552d..743d285b 100644 --- a/contracts/confidential-wrapper/.env.example +++ b/contracts/confidential-wrapper/.env.example @@ -44,12 +44,5 @@ CONFIDENTIAL_WRAPPER_UNDERLYING_DENY_LIST_SELECTOR_1=0x00000000 # bytes4 select CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_1='[]' # JSON array of observer addresses CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_1=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC # accounts[2] (address) -# ---------------------------------------------------------------------------- -# ConfidentialWrapper upgrade -# ---------------------------------------------------------------------------- - -# ConfidentialWrapper upgrade version label used by upgrade tasks -CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL=v2 - # 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/README.md b/contracts/confidential-wrapper/README.md index 90044d45..eaa7fcb3 100644 --- a/contracts/confidential-wrapper/README.md +++ b/contracts/confidential-wrapper/README.md @@ -33,14 +33,6 @@ Wraps standard ERC20 tokens into confidential ERC7984 tokens using FHE. Deployed | `CONFIDENTIAL_WRAPPER_INITIAL_OBSERVERS_{i}` | Optional JSON array of observer addresses to seed during initialization | | `CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_{i}` | Address allowed to call `pause()`, set during initialization; the zero address disables pausing | -### Task inputs (batch deploy upgrade implementations) - -| 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 | - > **Underlying deny-list configuration:** the selector alone carries enablement. > Consumers of `getUnderlyingDenyListSelector` determine enablement with `selector != 0`. A deny-list > getter whose selector is genuinely `0x00000000` can exist in theory but is indistinguishable from @@ -133,63 +125,38 @@ Verify all deployed confidential wrapper contracts on Etherscan. Reads wrapper n 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. +Deploy a new `ConfidentialWrapper` implementation contract without upgrading any proxy. The proxy upgrade is handled separately by the DAO. The artifact is saved as `ConfidentialWrapper_