import {Abi, AbiFunction} from 'abitype'; import type {Artifact, DeploymentConstruction, Deployment, Environment} from '@rocketh/core/types'; import type {EIP1193Account} from 'eip-1193'; import {encodeFunctionData, zeroAddress} from 'viem'; import {logs} from 'named-logs'; import {deploy, DeployOptions} from '@rocketh/deploy'; import {checkUpgradeIndex, replaceTemplateArgs, recordDescribesImplementation} from './utils.js'; import ERC1967Proxy from './hardhat-deploy-v1-artifacts/ERC1967Proxy.js'; import ERC173Proxy from './hardhat-deploy-v1-artifacts/EIP173Proxy.js'; import ERC173ProxyWithReceive from './hardhat-deploy-v1-artifacts/EIP173ProxyWithReceive.js'; import TransparentUpgradeableProxy from './hardhat-deploy-v1-artifacts/TransparentUpgradeableProxy.js'; import OptimizedTransparentUpgradeableProxy from './hardhat-deploy-v1-artifacts/OptimizedTransparentUpgradeableProxy.js'; import DefaultProxyAdmin from './hardhat-deploy-v1-artifacts/ProxyAdmin.js'; import {execute, read} from '@rocketh/read-execute'; import {mergeABIs} from '@rocketh/core/artifacts'; import {toJSONCompatibleLinkedData} from '@rocketh/core/json'; const logger = logs('@rocketh/proxy'); /** * The EIP-1967 IMPLEMENTATION slot: `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)`. * * Standardised precisely so that tooling can find a proxy's implementation without the * proxy having to expose a getter, which is what we do here — `eth_getStorageAt` on this * slot, rather than an `implementation()` call that many proxies deliberately do not have * (or hide from the admin). The minus-one is part of the EIP: it makes the slot provably * outside the range any Solidity mapping or array can compute, so it cannot collide with * the implementation contract's own storage layout. * * @see https://eips.ethereum.org/EIPS/eip-1967 */ const EIP1967_IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' as const; /** * The EIP-1967 ADMIN slot: `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)`. * * Read here to find out who may upgrade a proxy, as the fallback for the cases where * asking the proxy (an `owner()` / `admin()` call) does not work — a transparent proxy * routes non-admin calls to the implementation, so the answer depends on who is asking. * * @see https://eips.ethereum.org/EIPS/eip-1967 */ const EIP1967_ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103' as const; export type {Abi, AbiFunction, Artifact, DeploymentConstruction, Deployment, Environment}; export type PredefinedProxyContract = | 'ERC173Proxy' | 'ERC173ProxyWithReceive' | 'UUPS' | 'SharedAdminOpenZeppelinTransparentProxy' | 'SharedAdminOptimizedTransparentProxy'; type DeployMutuallyExclusiveOptions = {alwaysOverride?: boolean} | {strictBytecodeMatch?: boolean}; export type ProxyDeployOptions = Omit< DeployOptions, 'skipIfAlreadyDeployed' | 'alwaysOverride' | 'strictBytecodeMatch' > & DeployMutuallyExclusiveOptions & { proxyDisabled?: boolean; owner?: EIP1193Account; execute?: | string | { methodName: string; args?: any[]; } | { init: | string | { methodName: string; args?: any[]; }; onUpgrade?: | string | { methodName: string; args?: any[]; }; }; upgradeIndex?: number; checkProxyAdmin?: boolean; checkABIConflict?: boolean; deterministicImplementation?: boolean; proxyContract?: | PredefinedProxyContract | ({type: PredefinedProxyContract} & { type: 'SharedAdminOpenZeppelinTransparentProxy' | 'SharedAdminOptimizedTransparentProxy'; proxyAdminName?: string; // TODO allow custom proxyAdmin artifact? }) | { type: 'custom'; artifact: Artifact; args?: ('{implementation}' | '{admin}' | '{data}')[]; // default to ['{implementation}', '{admin}', '{data}'] // TODO allow viaAdminContract for custom proxy artifacts // We could just use boolean | {proxyAdminName: string} // viaAdminContract?: // | string // | { // name: string; // artifact?: string | ArtifactData; // }; // viaAdminContract = { // artifactName: 'DefaultProxyAdmin', // proxyAdminName: // (typeof options.proxyContract === 'object' && options.proxyContract.proxyAdminName) || // 'DefaultProxyAdmin', // }; }; }; export type ImplementationDeployer = ( name: string, args: Omit, 'artifact'>, options?: DeployOptions, ) => Promise>; // TODO omit nonce ? // TODO omit chain ? same for rocketh-deploy export type ProxyEnhancedDeploymentConstruction = Omit, 'artifact'> & { artifact: Artifact | ImplementationDeployer; }; export type ProxyEnhancedDeploymentConstructionWithoutFunction = Omit< DeploymentConstruction, 'artifact' > & { artifact: Artifact; }; export type DeployViaProxyFunction = ( name: string, params: ProxyEnhancedDeploymentConstruction, options?: ProxyDeployOptions, ) => Promise>; export function deployViaProxy( env: Environment, ): ( name: string, params: ProxyEnhancedDeploymentConstruction, options?: ProxyDeployOptions, ) => Promise> { const _deploy = deploy(env); const _read = read(env); const _execute = execute(env); return async ( name: string, params: ProxyEnhancedDeploymentConstruction, options?: ProxyDeployOptions, ) => { const alwaysOverride = options && 'alwaysOverride' in options && options.alwaysOverride; const strictBytecodeMatch = !alwaysOverride && options && 'strictBytecodeMatch' in options && options.strictBytecodeMatch; const skipIfAlreadyDeployed = alwaysOverride ? false : true; let optionsForImplementation = options ? { strictBytecodeMatch: strictBytecodeMatch, alwaysOverride: alwaysOverride, deterministic: options.deterministic || options.deterministicImplementation, libraries: options.libraries, } : undefined; let optionsForProxy = options ? ((options) => { const { owner, checkABIConflict, checkProxyAdmin, execute, deterministicImplementation, proxyContract, proxyDisabled, upgradeIndex, linkedData, ...rest } = options; return {...rest, strictBytecodeMatch: false}; // strictBytecodeMatch is never applied to proxy })(options) : undefined; const proxyName = `${name}_Proxy`; const implementationName = `${name}_Implementation`; let existingDeployment = env.getOrNull(name); if (options?.proxyDisabled) { // WITH NO PROXY, THE DEPLOYMENT IS THE IMPLEMENTATION, so it takes the implementation's // options. It used to take the PROXY's, which decided two things on its behalf that a // proxy needs and a bare contract must not inherit: a forced `skipIfAlreadyDeployed`, // which skipped on NAME alone and silently left a recompiled contract undeployed with // nothing to upgrade it afterwards; and a forced `strictBytecodeMatch: false`, which // is there so a metadata-only diff cannot trigger an UPGRADE, and which here just // discarded the caller's own setting for their own contract. It also dropped // `deterministicImplementation`, which is the only implementation there is. if (typeof params.artifact === 'function') { return params.artifact(name, params, { ...optionsForImplementation, linkedData: options.linkedData, }); } else { return _deploy(name, params as DeploymentConstruction, { ...optionsForImplementation, linkedData: options.linkedData, }); } } const deployResult = checkUpgradeIndex(existingDeployment, options?.upgradeIndex); if (deployResult) { return deployResult; } const {account, artifact, args, ...viemArgs} = params; if (!account) { throw new Error(`no account specified`); } const address = env.resolveAccount(account); let viaAdminContract: {artifactName: 'DefaultProxyAdmin'; proxyAdminName: string} | undefined; let proxyArgsTemplate = ['{implementation}', '{admin}', '{data}']; let proxyArtifact: Artifact = ERC173Proxy; let checkABIConflict: boolean | string[] = ['supportsInterface']; let checkProxyAdmin = true; if (options?.proxyContract) { if (typeof options.proxyContract !== 'string' && options.proxyContract.type === 'custom') { proxyArtifact = options.proxyContract.artifact; proxyArgsTemplate = options.proxyContract.args || ['{implementation}', '{admin}', '{data}']; } else { const proxyContractDefinition = typeof options.proxyContract === 'string' ? options.proxyContract : options.proxyContract.type; switch (proxyContractDefinition) { case 'ERC173Proxy': proxyArtifact = ERC173Proxy; proxyArgsTemplate = ['{implementation}', '{admin}', '{data}']; break; case 'ERC173ProxyWithReceive': proxyArtifact = ERC173ProxyWithReceive; proxyArgsTemplate = ['{implementation}', '{admin}', '{data}']; break; case 'UUPS': checkABIConflict = false; checkProxyAdmin = false; proxyArtifact = ERC1967Proxy; proxyArgsTemplate = ['{implementation}', '{data}']; break; case 'SharedAdminOpenZeppelinTransparentProxy': checkABIConflict = false; proxyArtifact = TransparentUpgradeableProxy; proxyArgsTemplate = ['{implementation}', '{admin}', '{data}']; viaAdminContract = { artifactName: 'DefaultProxyAdmin', proxyAdminName: (typeof options.proxyContract === 'object' && options.proxyContract.proxyAdminName) || 'DefaultProxyAdmin', }; break; case 'SharedAdminOptimizedTransparentProxy': checkABIConflict = false; proxyArtifact = OptimizedTransparentUpgradeableProxy; proxyArgsTemplate = ['{implementation}', '{admin}', '{data}']; viaAdminContract = { artifactName: 'DefaultProxyAdmin', proxyAdminName: (typeof options.proxyContract === 'object' && options.proxyContract.proxyAdminName) || 'DefaultProxyAdmin', }; break; default: throw new Error(`unknown proxy contract ${options.proxyContract}`); } } } checkABIConflict = options?.checkABIConflict ?? checkABIConflict; checkProxyAdmin = options?.checkProxyAdmin ?? checkProxyAdmin; const implementationDeployment = typeof params.artifact === 'function' ? await params.artifact(implementationName, {...params}, optionsForImplementation) : await _deploy( implementationName, { ...viemArgs, args, artifact, account: address, } as DeploymentConstruction, optionsForImplementation, ); // logger.info(`implementation at ${implementationDeployment.address}`, `${implementationName}`); const { address: implementationAddress, argsData: implementationArgsData, transaction, newlyDeployed: implementationNewlyDeployed, ...artifactFromImplementationDeployment } = implementationDeployment; // TODO throw specific error if artifact not found const artifactToUse = artifactFromImplementationDeployment; const {mergedABI} = mergeABIs( [ {name: implementationName, abi: artifactFromImplementationDeployment.abi}, {name: proxyName, abi: proxyArtifact.abi}, ], {checkForConflicts: checkABIConflict}, ); // logger.info(`existingDeployment at ${existingDeployment?.address}`); const expectedOwner = options?.owner || address; let proxyAdmin = expectedOwner; let proxyAdminContract: | { deployment: Deployment; owner: `0x${string}`; } | undefined; if (viaAdminContract?.artifactName === 'DefaultProxyAdmin') { const proxyAdminOwner = expectedOwner; const proxyAdminName = viaAdminContract.proxyAdminName; let proxyAdminDeployed: Deployment | null = env.getOrNull(proxyAdminName); if (!proxyAdminDeployed) { const proxyAdminDeployment = await _deploy( proxyAdminName, { ...params, artifact: DefaultProxyAdmin, args: [proxyAdminOwner], }, { // TODO use optionsForProxy instead ? alwaysOverride, deterministic: options?.deterministic, skipIfAlreadyDeployed, }, ); proxyAdminDeployed = proxyAdminDeployment; } const currentProxyAdminOwner = await _read(proxyAdminDeployed, {functionName: 'owner'}); if (currentProxyAdminOwner.toLowerCase() !== expectedOwner.toLowerCase()) { throw new Error(`To change owner/admin, you need to call transferOwnership on ${proxyAdminName}`); } if (currentProxyAdminOwner === zeroAddress) { throw new Error(`The Proxy Admin (${proxyAdminName}) belongs to no-one. The Proxy cannot be upgraded anymore`); } proxyAdmin = proxyAdminDeployed.address; proxyAdminContract = { deployment: proxyAdminDeployed, owner: currentProxyAdminOwner.toLowerCase() as `0x${string}`, }; } let postUpgradeCalldata: `0x${string}` | undefined; if (options?.execute) { let execution: | { methodName: string; args: any[]; } | undefined; if (typeof options.execute == 'string') { execution = { methodName: options.execute, args: args as any[], }; } else if ('methodName' in options.execute) { execution = { methodName: options.execute.methodName, args: options.execute.args || (args as any[]), }; } else { if (existingDeployment) { if (typeof options.execute.onUpgrade === 'string') { execution = { methodName: options.execute.onUpgrade, args: args as any[], }; } else if (typeof options.execute.onUpgrade === 'object') { execution = { methodName: options.execute.onUpgrade.methodName, args: options.execute.onUpgrade.args || (args as any[]), }; } } else { if (typeof options.execute.init === 'string') { execution = { methodName: options.execute.init, args: args as any[], }; } else if (typeof options.execute.init === 'object') { execution = { methodName: options.execute.init.methodName, args: options.execute.init.args || (args as any[]), }; } } } if (execution) { const method: AbiFunction | undefined = artifactToUse.abi.find( (v) => v.type === 'function' && v.name === execution.methodName, ) as AbiFunction; if (method) { postUpgradeCalldata = encodeFunctionData({ ...viemArgs, args: execution.args, account: address, abi: [method], functionName: method.name, }); } else { throw new Error(`Method ${execution.methodName} not found in artifact provided for ${name}`); } } } // let preUpgradeCalldata: `0x${string}` | undefined; // if (options?.preExecute) { // const method: AbiFunction | undefined = artifactToUse.abi.find( // (v) => v.type === 'function' && v.name === options.preExecute // ) as AbiFunction; // if (method) { // preUpgradeCalldata = encodeFunctionData({...viemArgs, account, abi: [method], functionName: method.name}); // } // } if (!existingDeployment) { const {newlyDeployed, ...proxy} = await _deploy( proxyName, { ...params, artifact: proxyArtifact, args: replaceTemplateArgs(proxyArgsTemplate, { implementationAddress: implementationDeployment.address, proxyAdmin: proxyAdmin, data: postUpgradeCalldata ? postUpgradeCalldata : '0x', }), }, optionsForProxy, ); // logger.info(`proxy deployed at ${proxy.address}`); existingDeployment = await env.save(name, { ...proxy, ...artifactToUse, abi: mergedABI as unknown as TAbi, linkedData: toJSONCompatibleLinkedData(options?.linkedData), }); // logger.info(`saving as ${name}`); } else { const proxyDeployment = env.getOrNull(proxyName); if (!proxyDeployment) { throw new Error(`deployment for "${name}" exits but there is no proxy`); } const implementationSlotData = await env.network.provider.request({ method: 'eth_getStorageAt', params: [proxyDeployment.address, EIP1967_IMPLEMENTATION_SLOT, 'latest'], }); const currentImplementationAddress = `0x${implementationSlotData.substr(-40)}`; const upgradeNeeded = currentImplementationAddress.toLowerCase() !== implementationDeployment.address.toLowerCase(); if (upgradeNeeded) { // logger.info( // `different implementation old: ${currentImplementationAddress} new: ${implementationDeployment.address}, upgrade...`, // ); // let currentOwner: `0x${string}` | undefined; // try { // currentOwner = await env.read(proxyDeployment, {functionName: 'owner'}); // console.log({currentOwner}); // } catch { // currentOwner = undefined; // } // if (!currentOwner) { const ownerSlotData = await env.network.provider.request({ method: 'eth_getStorageAt', params: [proxyDeployment.address, EIP1967_ADMIN_SLOT, 'latest'], }); let currentOwner = `0x${ownerSlotData.substr(-40)}`; if (currentOwner === zeroAddress) { // FALLBACK, and the throw is SWALLOWED DELIBERATELY. An empty EIP-1967 admin slot // does not mean "no owner": a proxy may keep its owner somewhere else entirely // (ERC173 stores it in its own slot), so before concluding anything we ASK the // contract. That call legitimately fails for a proxy that has no `owner()` at all, // and "it has no such method" is an ANSWER here, not an error worth surfacing — the // question was only ever "can you tell me your owner?". // // Nothing is hidden by swallowing it: `currentOwner` stays the zero address, and the // very next check turns that into either a clear refusal ("The Proxy belongs to // no-one. It cannot be upgraded anymore") or the no-admin path. Logging the RPC // failure would put a scary line in front of every user of an ownerless proxy for a // case the code handles by design. try { const owner = await _read(existingDeployment as any, {functionName: 'owner'}); currentOwner = (owner as string).toLowerCase() as `0x${string}`; } catch (err) { // intentionally ignored — see above; the zero-address check below is the handler } } if (currentOwner === zeroAddress) { if (checkProxyAdmin) { throw new Error('The Proxy belongs to no-one. It cannot be upgraded anymore'); } } else if (currentOwner.toLowerCase() !== proxyAdmin.toLowerCase()) { throw new Error( `To change owner/admin, you need to call the proxy directly, it currently is ${currentOwner}`, ); } // if (preUpgradeCalldata) { // if (postUpgradeCalldata) { // await env.execute(proxyDeployment, { // account: address, // functionName: 'callAndUpgradeToAndCall', // args: [implementation.address, preUpgradeCalldata, postUpgradeCalldata], // value: 0n, // TODO // }); // } else { // await env.execute(proxyDeployment, { // account: address, // functionName: 'callAndUpgradeToAndCall', // args: [implementation.address, preUpgradeCalldata, '0x'], // value: 0n, // TODO // }); // } // } else const deploymentToUseForUpgrade = options?.proxyContract === 'UUPS' ? existingDeployment : proxyDeployment; let useUpgradeToAndCall = !!postUpgradeCalldata; if (!useUpgradeToAndCall) { if (!deploymentToUseForUpgrade.abi.find((v) => v.type === 'function' && v.name === 'upgradeTo')) { useUpgradeToAndCall = true; } } if (proxyAdminContract) { if (useUpgradeToAndCall) { await _execute(proxyAdminContract.deployment, { account: proxyAdminContract.owner, functionName: 'upgradeAndCall', args: [proxyDeployment.address, implementationDeployment.address, postUpgradeCalldata || '0x'], value: 0n, // TODO }); } else { await _execute(proxyAdminContract.deployment, { account: proxyAdminContract.owner, functionName: 'upgrade', args: [proxyDeployment.address, implementationDeployment.address], }); } } else { if (useUpgradeToAndCall) { await _execute(deploymentToUseForUpgrade, { account: currentOwner, functionName: 'upgradeToAndCall', args: [implementationDeployment.address, postUpgradeCalldata || '0x'], value: 0n, // TODO }); } else { await _execute(deploymentToUseForUpgrade, { account: currentOwner, functionName: 'upgradeTo', args: [implementationDeployment.address], }); } } } // The record describes the CHAIN, not this run, so it is written whenever the two // agree, however they came to. See `Environment.save` in `@rocketh/core` for the // counter rule and `recordDescribesImplementation` for what "agree" compares. // // `upgradeNeeded ||` is the part that is specific to this call site and must not be // folded into the guard: an upgrade this run performed always saves, because two // implementations can differ while their ABIs are identical and skipping the save // there would freeze `numDeployments`, which `upgradeIndex` reads. if (upgradeNeeded || !recordDescribesImplementation(existingDeployment, mergedABI, artifactToUse)) { existingDeployment = await env.save(name, { ...proxyDeployment, ...artifactToUse, abi: mergedABI as unknown as TAbi, linkedData: toJSONCompatibleLinkedData(options?.linkedData), }); // logger.info(`saving as ${name}`); } } return existingDeployment; }; }