diff --git a/packages/bridge-contracts/deploy/deployOFT.ts b/packages/bridge-contracts/deploy/deployOFT.ts index c35a8bd..cb38709 100644 --- a/packages/bridge-contracts/deploy/deployOFT.ts +++ b/packages/bridge-contracts/deploy/deployOFT.ts @@ -5,25 +5,43 @@ * 1. GoodDollarOFTMinterBurner - DAO-upgradeable contract that handles minting and burning of GoodDollar tokens for OFT * 2. GoodDollarOFTAdapter - Upgradeable LayerZero OFT adapter that wraps GoodDollar token for cross-chain transfers * - * Steps: - * 1. Deploy both implementations (deterministic, salt = bytecode hash) - * 2. Predict both proxy addresses via GoodProtocol ProxyFactory1967.getDeploymentAddress(salt, deployer) - * 3. Deploy + initialize each proxy atomically via ProxyFactory1967.deployProxy(salt, impl, initData) + * Everything goes through CreateX (https://github.com/pcaversaccio/createx), the universal deployer + * that sits at the same address on every chain: + * 1. Deploy both implementations with CreateX CREATE2, salt entropy = init code hash + * (the adapter implementation takes chain-specific constructor args, so its address is per-chain) + * 2. Predict both proxy addresses with CreateX CREATE3 - the address comes from (CreateX, deployer, + * salt label) only, so the proxies get the SAME address on every chain, and both addresses are + * known up front, which breaks the MinterBurner <-> adapter init cycle + * 3. Deploy + initialize each ERC1967 proxy atomically via CreateX.deployCreate3AndInit * - MinterBurner.initialize(nameService, adapter) * - Adapter.initialize(token, minterBurner, deployer, Avatar as feeRecipient) * 4. verifyOftDeployment: impl slots, owner, token, minterBurner, feeRecipient, operators(adapter) * - * Note: proxy addresses depend on the per-chain ProxyFactory address, so they differ between chains. + * Note: the deployer EOA is part of the CREATE3 salt, so every chain must be deployed from the same + * signer to land on the same address. Run `hardhat oft:addresses` to see the addresses beforehand. */ import { DeployFunction } from 'hardhat-deploy/types'; import { ethers } from 'hardhat'; -import { BigNumber } from 'ethers'; import Contracts from '@gooddollar/goodprotocol/releases/deployment.json'; import { getImplementationAddress } from '@openzeppelin/upgrades-core'; import { verifyContract } from './utils/verifyContract'; import { getLzEndpoint } from './utils/getLzEndpoint'; import { verifyOftDeployment } from './utils/verifyOftDeployment'; +import { + CreateXDeployment, + computeCreate3Address, + createXSalt, + deployCreate2IfMissing, + deployCreate3AndInitIfMissing, + getCreateX, +} from './utils/createX'; +import { oftProxySalts, oftSaltLabels } from './utils/oftSalts'; + +/// GoodProtocol's ERC1967 proxy: deployed empty, then initialized with (implementation, initData). +/// It is the proxy every other GoodDollar contract uses, and it has no constructor args, so the +/// same init code is used on every chain. +export const ERC1967_PROXY_FQN = '@gooddollar/goodprotocol/contracts/utils/ProxyFactory1967.sol:ERC1967Proxy'; const func: DeployFunction = async function (hre) { const { deployments, network } = hre; @@ -94,89 +112,96 @@ const func: DeployFunction = async function (hre) { } console.log('✅ Verified Avatar address:', avatarAddress); - let isDevelopment = false; - if (network.name.includes('development')) { - isDevelopment = true; - } + // Reverts unless the code at the canonical CreateX address really is CreateX + const createX = await getCreateX(root); + console.log('Using CreateX at', createX.address); + + const minterBurnerFactory = await ethers.getContractFactory('GoodDollarOFTMinterBurner'); + const oftAdapterFactory = await ethers.getContractFactory('GoodDollarOFTAdapter'); + + // Implementations: CREATE2 through CreateX, with the init code hash as salt entropy so a new + // version (or different constructor args) maps to a different implementation address. + const minterBurnerImplInitCode = minterBurnerFactory.getDeployTransaction().data as string; + const oftAdapterImplInitCode = oftAdapterFactory.getDeployTransaction(tokenAddress, lzEndpoint).data as string; + + const logDeployment = (name: string, deployment: CreateXDeployment) => + console.log( + `${name} ${deployment.reused ? 'already deployed' : 'deployed'}`, + deployment.address, + deployment.transactionHash ?? '', + ); - // CREATE2 salt for implementations: - // hardhat-deploy uses `deterministicDeployment` as the CREATE2 salt. - // We derive it from the contract's compiled bytecode so version changes - // map to different deterministic implementation addresses. - const minterBurnerArtifact = await hre.artifacts.readArtifact('GoodDollarOFTMinterBurner'); - const minterBurnerImplSalt = ethers.utils.keccak256(minterBurnerArtifact.bytecode); - const oftAdapterArtifact = await hre.artifacts.readArtifact('GoodDollarOFTAdapter'); - const oftAdapterImplSalt = ethers.utils.keccak256(oftAdapterArtifact.bytecode); - - const minterBurnerImpl = await deployments.deploy('GoodDollarOFTMinterBurner_Implementation', { - contract: 'GoodDollarOFTMinterBurner', - from: root.address, - deterministicDeployment: minterBurnerImplSalt, - log: true, + const minterBurnerImpl = await deployCreate2IfMissing( + createX, + root.address, + createXSalt(root.address, ethers.utils.keccak256(minterBurnerImplInitCode)), + minterBurnerImplInitCode, + ); + logDeployment('GoodDollarOFTMinterBurner implementation', minterBurnerImpl); + await deployments.save('GoodDollarOFTMinterBurner_Implementation', { + address: minterBurnerImpl.address, + abi: (await hre.artifacts.readArtifact('GoodDollarOFTMinterBurner')).abi, + args: [], + transactionHash: minterBurnerImpl.transactionHash, + receipt: minterBurnerImpl.receipt as any, }); - console.log('GoodDollarOFTMinterBurner implementation', minterBurnerImpl.address); - const oftAdapterImpl = await deployments.deploy('GoodDollarOFTAdapter_Implementation', { - contract: 'GoodDollarOFTAdapter', - from: root.address, - deterministicDeployment: oftAdapterImplSalt, - log: true, + const oftAdapterImpl = await deployCreate2IfMissing( + createX, + root.address, + createXSalt(root.address, ethers.utils.keccak256(oftAdapterImplInitCode)), + oftAdapterImplInitCode, + ); + logDeployment('GoodDollarOFTAdapter implementation', oftAdapterImpl); + await deployments.save('GoodDollarOFTAdapter_Implementation', { + address: oftAdapterImpl.address, + abi: (await hre.artifacts.readArtifact('GoodDollarOFTAdapter')).abi, args: [tokenAddress, lzEndpoint], + transactionHash: oftAdapterImpl.transactionHash, + receipt: oftAdapterImpl.receipt as any, }); - console.log('GoodDollarOFTAdapter implementation', oftAdapterImpl.address); - // ProxyFactory1967 deploys and initializes in one tx, with a CREATE2 salt bound to the deployer (no init front-running) - const proxyFactoryAddress = goodProtocolContracts.ProxyFactory; - if (!proxyFactoryAddress) { - throw new Error(`ProxyFactory address not found in GoodProtocol deployment for network ${networkName}`); - } - const proxyFactory = await ethers.getContractAt( - [ - 'function deployProxy(uint256 _salt, address _logic, bytes _data) returns (address)', - 'function getDeploymentAddress(uint256 _salt, address _sender) view returns (address)', - ], - proxyFactoryAddress, - ); + // Proxies: CREATE3 through CreateX. The address only depends on (CreateX, deployer, salt), not on + // the implementation address, so it is identical on every chain and known before deploying. + const proxyArtifact = await hre.artifacts.readArtifact(ERC1967_PROXY_FQN); + const proxyInterface = new ethers.utils.Interface(proxyArtifact.abi); + const salts = oftProxySalts(networkName, root.address); - const envPrefix = isDevelopment ? 'Development' : 'Production'; - const minterBurnerProxySalt = ethers.BigNumber.from( - ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`${envPrefix}-GoodDollarOFTMinterBurnerV1`)), - ); - const oftAdapterProxySalt = ethers.BigNumber.from( - ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`${envPrefix}-GoodDollarOFTAdapterV1`)), - ); - // Both proxy addresses are known before deployment, which breaks the MinterBurner <-> adapter init cycle - const minterBurnerAddress: string = await proxyFactory.getDeploymentAddress(minterBurnerProxySalt, root.address); - const oftAdapterAddress: string = await proxyFactory.getDeploymentAddress(oftAdapterProxySalt, root.address); - - const deployProxyIfMissing = async (name: string, address: string, salt: BigNumber, logic: string, initData: string) => { - if ((await ethers.provider.getCode(address)) !== '0x') { - console.log(`${name} proxy already deployed`, address); - } else { - const tx = await proxyFactory.deployProxy(salt, logic, initData); - await tx.wait(); - console.log(`${name} proxy deployed and initialized`, address, tx.hash); - } - await deployments.save(name, { address, abi: (await hre.artifacts.readArtifact(name)).abi }); + const minterBurnerAddress = computeCreate3Address(root.address, salts.minterBurner); + const oftAdapterAddress = computeCreate3Address(root.address, salts.adapter); + + const deployProxyIfMissing = async (name: string, salt: string, logic: string, initData: string) => { + const deployment = await deployCreate3AndInitIfMissing( + createX, + root.address, + salt, + proxyArtifact.bytecode, + proxyInterface.encodeFunctionData('initialize', [logic, initData]), + ); + logDeployment(`${name} proxy`, deployment); + await deployments.save(name, { + address: deployment.address, + abi: (await hre.artifacts.readArtifact(name)).abi, + transactionHash: deployment.transactionHash, + receipt: deployment.receipt as any, + implementation: logic, + }); + return deployment.address; }; - const minterBurnerInterface = (await ethers.getContractFactory('GoodDollarOFTMinterBurner')).interface; await deployProxyIfMissing( 'GoodDollarOFTMinterBurner', - minterBurnerAddress, - minterBurnerProxySalt, + salts.minterBurner, minterBurnerImpl.address, - minterBurnerInterface.encodeFunctionData('initialize', [nameServiceAddress, oftAdapterAddress]), + minterBurnerFactory.interface.encodeFunctionData('initialize', [nameServiceAddress, oftAdapterAddress]), ); // Deployer owns the adapter until oft:configure hands owner + LZ delegate to the Avatar; fees go to the Avatar - const oftAdapterInterface = (await ethers.getContractFactory('GoodDollarOFTAdapter')).interface; await deployProxyIfMissing( 'GoodDollarOFTAdapter', - oftAdapterAddress, - oftAdapterProxySalt, + salts.adapter, oftAdapterImpl.address, - oftAdapterInterface.encodeFunctionData('initialize', [ + oftAdapterFactory.interface.encodeFunctionData('initialize', [ tokenAddress, minterBurnerAddress, root.address, @@ -199,13 +224,18 @@ const func: DeployFunction = async function (hre) { () => undefined, ); + const saltLabels = oftSaltLabels(networkName); + console.log('\n=== Deployment Summary ==='); console.log('Network:', networkName); - console.log('GoodDollarOFTMinterBurner:', minterBurnerAddress, '(upgradeable)'); + console.log('Deployer (part of the CREATE3 address):', root.address); + console.log('GoodDollarOFTMinterBurner:', minterBurnerAddress, '(upgradeable, same address on every chain)'); + console.log(' Salt label:', saltLabels.minterBurner); if (minterBurnerImplAddress) { console.log(' Implementation:', minterBurnerImplAddress); } - console.log('GoodDollarOFTAdapter:', oftAdapterAddress, '(upgradeable)'); + console.log('GoodDollarOFTAdapter:', oftAdapterAddress, '(upgradeable, same address on every chain)'); + console.log(' Salt label:', saltLabels.adapter); if (oftAdapterImplAddress) { console.log(' Implementation:', oftAdapterImplAddress); } diff --git a/packages/bridge-contracts/deploy/upgradeOFT.ts b/packages/bridge-contracts/deploy/upgradeOFT.ts index 5a65150..f7157e9 100644 --- a/packages/bridge-contracts/deploy/upgradeOFT.ts +++ b/packages/bridge-contracts/deploy/upgradeOFT.ts @@ -4,7 +4,8 @@ * Same flow as deployOFT.ts (no hardhat-upgrades): * - Reads proxy addresses from hardhat-deploy artifacts (`deployments/`) for the current network * - Resolves token and LayerZero endpoint from GoodProtocol deployment (same as deployOFT) - * - Deploys new implementations (OFT adapter with constructor args, MinterBurner with none) + * - Deploys new implementations through CreateX (same universal deployer as deployOFT; the adapter + * takes constructor args, the MinterBurner none) * - Upgrades each proxy (UUPS) through the DAO: Controller.genericCall when the deployer is a registered scheme, * otherwise prints the genericCall payloads to submit as a DAO proposal * @@ -21,6 +22,7 @@ import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; import { getImplementationAddress } from "@openzeppelin/upgrades-core"; import { getOftDeploymentAddresses } from "./utils/getOftDeploymentAddresses"; import { getLzEndpoint } from "./utils/getLzEndpoint"; +import { createXSalt, deployCreate2IfMissing, getCreateX } from "./utils/createX"; const func: DeployFunction = async function (hre) { const { deployments, network } = hre; @@ -111,25 +113,35 @@ const func: DeployFunction = async function (hre) { } }; - // CREATE2 salt for implementations: - // hardhat-deploy uses `deterministicDeployment` as the CREATE2 salt. - // We derive it from the contract's compiled bytecode. - const minterBurnerArtifact = await hre.artifacts.readArtifact("GoodDollarOFTMinterBurner"); - const minterBurnerImplSalt = ethers.utils.keccak256(minterBurnerArtifact.bytecode); - const oftAdapterArtifact = await hre.artifacts.readArtifact("GoodDollarOFTAdapter"); - const oftAdapterImplSalt = ethers.utils.keccak256(oftAdapterArtifact.bytecode); + // Implementations are CREATE2-deployed through CreateX, exactly as in deployOFT: the salt entropy + // is the init code hash, so a new version lands on a new implementation address, and re-running + // the upgrade with unchanged code reuses the existing one. + const createX = await getCreateX(root); + const minterBurnerFactory = await ethers.getContractFactory("GoodDollarOFTMinterBurner"); + const oftAdapterFactory = await ethers.getContractFactory("GoodDollarOFTAdapter"); + const minterBurnerImplInitCode = minterBurnerFactory.getDeployTransaction().data as string; + const oftAdapterImplInitCode = oftAdapterFactory.getDeployTransaction(tokenAddress, lzEndpoint).data as string; - // --- GoodDollarOFTAdapter: deploy new implementation via hardhat-deploy, then upgrade proxy --- + // --- GoodDollarOFTAdapter: deploy new implementation, then upgrade proxy --- console.log("\nDeploying new GoodDollarOFTAdapter implementation..."); - const oftAdapterImpl = await deployments.deploy("GoodDollarOFTAdapter_Implementation", { - contract: "GoodDollarOFTAdapter", - from: root.address, - deterministicDeployment: oftAdapterImplSalt, - log: true, + const oftAdapterImpl = await deployCreate2IfMissing( + createX, + root.address, + createXSalt(root.address, ethers.utils.keccak256(oftAdapterImplInitCode)), + oftAdapterImplInitCode + ); + console.log( + `GoodDollarOFTAdapter implementation ${oftAdapterImpl.reused ? "already deployed" : "deployed"}`, + oftAdapterImpl.address + ); + await deployments.save("GoodDollarOFTAdapter_Implementation", { + address: oftAdapterImpl.address, + abi: (await hre.artifacts.readArtifact("GoodDollarOFTAdapter")).abi, args: [tokenAddress, lzEndpoint], + transactionHash: oftAdapterImpl.transactionHash, + receipt: oftAdapterImpl.receipt as any, }); - console.log("GoodDollarOFTAdapter implementation", oftAdapterImpl.address); console.log("Upgrading GoodDollarOFTAdapter proxy..."); const oftAdapterProxyContract = await ethers.getContractAt( @@ -142,18 +154,25 @@ const func: DeployFunction = async function (hre) { } await upgradeProxy("GoodDollarOFTAdapter", oftAdapterProxy, oftAdapterImpl.address, adapterOwner === avatarAddress); - // --- GoodDollarOFTMinterBurner: deploy new implementation via hardhat-deploy, then upgrade proxy --- + // --- GoodDollarOFTMinterBurner: deploy new implementation, then upgrade proxy --- console.log("\nDeploying new GoodDollarOFTMinterBurner implementation..."); - const minterBurnerImpl = await deployments.deploy( - "GoodDollarOFTMinterBurner_Implementation", - { - contract: "GoodDollarOFTMinterBurner", - from: root.address, - deterministicDeployment: minterBurnerImplSalt, - log: true, - } + const minterBurnerImpl = await deployCreate2IfMissing( + createX, + root.address, + createXSalt(root.address, ethers.utils.keccak256(minterBurnerImplInitCode)), + minterBurnerImplInitCode ); - console.log("GoodDollarOFTMinterBurner implementation", minterBurnerImpl.address); + console.log( + `GoodDollarOFTMinterBurner implementation ${minterBurnerImpl.reused ? "already deployed" : "deployed"}`, + minterBurnerImpl.address + ); + await deployments.save("GoodDollarOFTMinterBurner_Implementation", { + address: minterBurnerImpl.address, + abi: (await hre.artifacts.readArtifact("GoodDollarOFTMinterBurner")).abi, + args: [], + transactionHash: minterBurnerImpl.transactionHash, + receipt: minterBurnerImpl.receipt as any, + }); console.log("Upgrading GoodDollarOFTMinterBurner proxy via the DAO avatar..."); await upgradeProxy("GoodDollarOFTMinterBurner", minterBurnerProxy, minterBurnerImpl.address, true); diff --git a/packages/bridge-contracts/deploy/utils/createX.ts b/packages/bridge-contracts/deploy/utils/createX.ts new file mode 100644 index 0000000..d8774dc --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/createX.ts @@ -0,0 +1,177 @@ +/*** + * Helpers for deploying through CreateX, the universal deployer that lives at the same address on + * every chain: https://github.com/pcaversaccio/createx + * + * Why: proxy addresses used to come from the per-chain GoodProtocol ProxyFactory1967, so the same + * contract got a different address on every chain. CreateX is at one address everywhere, and its + * CREATE3 deployments derive the address from (CreateX, guarded salt) only - not from the init code + * or the implementation address - so the OFT proxies get one address across all chains. + * + * Salt layout (CreateX `_guard`/`_parseSalt`): + * bytes20(deployer) | 0x00 | bytes11(entropy) + * - the deployer prefix enables permissioned deploy protection: only that EOA can use the salt, + * so nobody can squat our address on a chain we haven't deployed to yet + * - the 0x00 flag byte disables cross-chain redeploy protection, i.e. block.chainid is left out + * of the guarded salt, which is exactly what makes the address identical on every chain + * guarded salt = keccak256(bytes32(uint256(uint160(deployer))) ++ salt) + * + * Consequence: the deployer EOA is part of the address. Deploy every chain with the same signer. + */ + +import { BigNumber, Contract, Signer, providers, utils } from 'ethers'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import createxRuntime from './createxRuntime.json'; + +/// hardhat.config.ts registers the oft tasks, which reach this module, so the HRE can only be +/// pulled in lazily - importing it at module level breaks config loading (HH9). +async function getHre(): Promise { + return (await import('hardhat')) as unknown as HardhatRuntimeEnvironment; +} + +export const CREATEX_ADDRESS = '0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed'; + +/// keccak256 of the deployed CreateX runtime bytecode, as published by the CreateX project. +/// CreateX asks integrators to check this before interacting with it on any chain. +export const CREATEX_RUNTIME_CODE_HASH = '0xbd8a7ea8cfca7b4e5f5041d7d4b17bc317c5ce42cfbc42066a00cf26b43eb53f'; + +export const CREATEX_ABI = [ + 'function deployCreate2(bytes32 salt, bytes initCode) payable returns (address newContract)', + 'function deployCreate3(bytes32 salt, bytes initCode) payable returns (address newContract)', + 'function deployCreate3AndInit(bytes32 salt, bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', + 'function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) view returns (address computedAddress)', + 'function computeCreate3Address(bytes32 salt) view returns (address computedAddress)', + 'event ContractCreation(address indexed newContract)', +]; + +/// CreateX's CREATE3 proxy: CREATE2-deployed from the guarded salt, it then CREATEs the contract as +/// its first and only transaction (nonce 1). Addresses are computed here rather than through +/// CreateX's view functions, which keeps prediction free of RPC quirks (e.g. eth_call at a fork block). +const CREATE3_PROXY_INIT_CODE = '0x67363d3d37363d34f03d5260086018f3'; + +const LOCAL_NETWORKS = ['hardhat', 'localhost']; + +/** + * Builds a CreateX salt for `deployer`: permissioned (only `deployer` can use it) and without + * cross-chain redeploy protection (same address on every chain). + * @param entropy a label (e.g. "Production-GoodDollarOFTAdapterV1") or any hex string; it is hashed + * and truncated to the 11 entropy bytes. Changing it changes the resulting address. + */ +export function createXSalt(deployer: string, entropy: string): string { + const hashed = utils.isHexString(entropy) ? utils.keccak256(entropy) : utils.id(entropy); + return utils.hexConcat([utils.getAddress(deployer), '0x00', utils.hexDataSlice(hashed, 0, 11)]); +} + +/// Mirrors CreateX `_guard` for the permissioned, cross-chain-enabled salt layout above. +export function guardCreateXSalt(deployer: string, salt: string): string { + return utils.keccak256(utils.hexConcat([utils.hexZeroPad(utils.getAddress(deployer), 32), salt])); +} + +/** + * Makes CreateX usable on local networks (hardhat/localhost) by etching its runtime bytecode at the + * canonical address. CreateX stores its own address as an immutable, so the fetched runtime code is + * only valid at that same address - which is where we put it. + */ +export async function etchCreateX(): Promise { + const { ethers, network } = await getHre(); + if (!LOCAL_NETWORKS.includes(network.name)) return; + if ((await ethers.provider.getCode(CREATEX_ADDRESS)) !== '0x') return; + if (utils.keccak256(createxRuntime.runtimeCode) !== CREATEX_RUNTIME_CODE_HASH) { + throw new Error('createxRuntime.json does not match the published CreateX runtime code hash'); + } + await network.provider.send('hardhat_setCode', [CREATEX_ADDRESS, createxRuntime.runtimeCode]); +} + +/** + * Returns the CreateX instance, after checking that the code at the canonical address really is + * CreateX. On local networks CreateX is etched in first. + */ +export async function getCreateX(signerOrProvider?: Signer | providers.Provider): Promise { + await etchCreateX(); + + const { ethers, network } = await getHre(); + const code = await ethers.provider.getCode(CREATEX_ADDRESS); + if (code === '0x') { + throw new Error( + `CreateX is not deployed at ${CREATEX_ADDRESS} on network "${network.name}". ` + + `See https://github.com/pcaversaccio/createx#deployments for the chains it is available on.`, + ); + } + if (utils.keccak256(code) !== CREATEX_RUNTIME_CODE_HASH) { + throw new Error( + `Code at ${CREATEX_ADDRESS} on "${network.name}" is not CreateX ` + + `(runtime code hash ${utils.keccak256(code)}, expected ${CREATEX_RUNTIME_CODE_HASH})`, + ); + } + + return new Contract(CREATEX_ADDRESS, CREATEX_ABI, signerOrProvider ?? ethers.provider); +} + +/// CREATE3 address for `salt`: depends on the deployer and salt only, never on the init code. +export function computeCreate3Address(deployer: string, salt: string): string { + const proxy = utils.getCreate2Address( + CREATEX_ADDRESS, + guardCreateXSalt(deployer, salt), + utils.keccak256(CREATE3_PROXY_INIT_CODE), + ); + return utils.getContractAddress({ from: proxy, nonce: 1 }); +} + +/// CREATE2 address for `salt` and `initCode`; changes whenever the init code (incl. constructor args) changes. +export function computeCreate2Address(deployer: string, salt: string, initCode: string): string { + return utils.getCreate2Address(CREATEX_ADDRESS, guardCreateXSalt(deployer, salt), utils.keccak256(initCode)); +} + +export type CreateXDeployment = { + address: string; + /// undefined when the contract was already deployed at the predicted address + transactionHash?: string; + receipt?: providers.TransactionReceipt; + reused: boolean; +}; + +/** + * CREATE2-deploys `initCode` through CreateX, or reports the existing contract when the predicted + * address is already taken (so deploy scripts stay re-runnable). + */ +export async function deployCreate2IfMissing( + createX: Contract, + deployer: string, + salt: string, + initCode: string, +): Promise { + const { ethers } = await getHre(); + const address = computeCreate2Address(deployer, salt, initCode); + if ((await ethers.provider.getCode(address)) !== '0x') { + return { address, reused: true }; + } + + const tx = await createX.deployCreate2(salt, initCode); + const receipt = await tx.wait(); + return { address, transactionHash: tx.hash, receipt, reused: false }; +} + +/** + * CREATE3-deploys `initCode` through CreateX and calls `data` on the new contract in the same + * transaction, or reports the existing contract when the predicted address is already taken. + * Atomic deploy+initialize means an uninitialized proxy can never be front-run. + */ +export async function deployCreate3AndInitIfMissing( + createX: Contract, + deployer: string, + salt: string, + initCode: string, + data: string, +): Promise { + const { ethers } = await getHre(); + const address = computeCreate3Address(deployer, salt); + if ((await ethers.provider.getCode(address)) !== '0x') { + return { address, reused: true }; + } + + const tx = await createX.deployCreate3AndInit(salt, initCode, data, { + constructorAmount: BigNumber.from(0), + initCallAmount: BigNumber.from(0), + }); + const receipt = await tx.wait(); + return { address, transactionHash: tx.hash, receipt, reused: false }; +} diff --git a/packages/bridge-contracts/deploy/utils/createxRuntime.json b/packages/bridge-contracts/deploy/utils/createxRuntime.json new file mode 100644 index 0000000..77a7897 --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/createxRuntime.json @@ -0,0 +1,6 @@ +{ + "_comment": "CreateX (https://github.com/pcaversaccio/createx) deployed runtime bytecode, read from the canonical 0xba5Ed0...ba5Ed deployment. Only used to etch CreateX onto local hardhat networks; keccak256(runtimeCode) must equal runtimeCodeHash, which is the hash published by the CreateX project.", + "address": "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed", + "runtimeCodeHash": "0xbd8a7ea8cfca7b4e5f5041d7d4b17bc317c5ce42cfbc42066a00cf26b43eb53f", + "runtimeCode": "0x60806040526004361061018a5760003560e01c806381503da1116100d6578063d323826a1161007f578063e96deee411610059578063e96deee414610395578063f5745aba146103a8578063f9664498146103bb57600080fd5b8063d323826a1461034f578063ddda0acb1461036f578063e437252a1461038257600080fd5b80639c36a286116100b05780639c36a28614610316578063a7db93f214610329578063c3fe107b1461033c57600080fd5b806381503da1146102d0578063890c283b146102e357806398e810771461030357600080fd5b80632f990e3f116101385780636cec2536116101125780636cec25361461027d57806374637a7a1461029d5780637f565360146102bd57600080fd5b80632f990e3f1461023757806331a7c8c81461024a57806342d654fc1461025d57600080fd5b806327fe18221161016957806327fe1822146101f15780632852527a1461020457806328ddd0461461021757600080fd5b8062d84acb1461018f57806326307668146101cb57806326a32fc7146101de575b600080fd5b6101a261019d366004612915565b6103ce565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b6101a26101d9366004612994565b6103e6565b6101a26101ec3660046129db565b610452565b6101a26101ff3660046129db565b6104de565b6101a2610212366004612a39565b610539565b34801561022357600080fd5b506101a2610232366004612a90565b6106fe565b6101a2610245366004612aa9565b61072a565b6101a2610258366004612aa9565b6107bb565b34801561026957600080fd5b506101a2610278366004612b1e565b6107c9565b34801561028957600080fd5b506101a2610298366004612a90565b610823565b3480156102a957600080fd5b506101a26102b8366004612b4a565b61084f565b6101a26102cb3660046129db565b611162565b6101a26102de366004612b74565b6111e8565b3480156102ef57600080fd5b506101a26102fe366004612bac565b611276565b6101a2610311366004612bce565b6112a3565b6101a2610324366004612994565b611505565b6101a2610337366004612c49565b6116f1565b6101a261034a366004612aa9565b611964565b34801561035b57600080fd5b506101a261036a366004612cd9565b6119ed565b6101a261037d366004612c49565b611a17565b6101a2610390366004612bce565b611e0c565b6101a26103a3366004612915565b611e95565b6101a26103b6366004612bce565b611ea4565b6101a26103c9366004612b74565b611f2d565b60006103dd8585858533611a17565b95945050505050565b6000806103f2846120db565b90508083516020850134f59150610408826123d3565b604051819073ffffffffffffffffffffffffffffffffffffffff8416907fb8fda7e00c6b06a2b54e58521bc5894fee35f1090e5a3bb6390bfe2b98b497f790600090a35092915050565b60006104d86104d260408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b836103e6565b92915050565b600081516020830134f090506104f3816123d3565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b51190600090a2919050565b600080610545856120db565b905060008460601b90506040517f3d602d80600a3d3981f3363d3d373d3d3d363d7300000000000000000000000081528160148201527f5af43d82803e903d91602b57fd5bf300000000000000000000000000000000006028820152826037826000f593505073ffffffffffffffffffffffffffffffffffffffff8316610635576040517fc05cee7a00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed1660048201526024015b60405180910390fd5b604051829073ffffffffffffffffffffffffffffffffffffffff8516907fb8fda7e00c6b06a2b54e58521bc5894fee35f1090e5a3bb6390bfe2b98b497f790600090a36000808473ffffffffffffffffffffffffffffffffffffffff1634876040516106a19190612d29565b60006040518083038185875af1925050503d80600081146106de576040519150601f19603f3d011682016040523d82523d6000602084013e6106e3565b606091505b50915091506106f382828961247d565b505050509392505050565b60006104d87f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed8361084f565b60006107b36107aa60408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b85858533611a17565b949350505050565b60006107b3848484336112a3565b60006040518260005260ff600b53836020527f21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f6040526055600b20601452806040525061d694600052600160345350506017601e20919050565b60006104d8827f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed6107c9565b600060607f9400000000000000000000000000000000000000000000000000000000000000610887600167ffffffffffffffff612d45565b67ffffffffffffffff16841115610902576040517f3c55ab3b00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b836000036109c7576040517fd60000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f800000000000000000000000000000000000000000000000000000000000000060368201526037015b6040516020818303038152906040529150611152565b607f8411610a60576040517fd60000000000000000000000000000000000000000000000000000000000000060208201527fff0000000000000000000000000000000000000000000000000000000000000080831660218301527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606088901b16602283015260f886901b1660368201526037016109b1565b60ff8411610b1f576040517fd70000000000000000000000000000000000000000000000000000000000000060208201527fff0000000000000000000000000000000000000000000000000000000000000080831660218301527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606088901b1660228301527f8100000000000000000000000000000000000000000000000000000000000000603683015260f886901b1660378201526038016109b1565b61ffff8411610bff576040517fd80000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f820000000000000000000000000000000000000000000000000000000000000060368201527fffff00000000000000000000000000000000000000000000000000000000000060f086901b1660378201526039016109b1565b62ffffff8411610ce0576040517fd90000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f830000000000000000000000000000000000000000000000000000000000000060368201527fffffff000000000000000000000000000000000000000000000000000000000060e886901b166037820152603a016109b1565b63ffffffff8411610dc2576040517fda0000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f840000000000000000000000000000000000000000000000000000000000000060368201527fffffffff0000000000000000000000000000000000000000000000000000000060e086901b166037820152603b016109b1565b64ffffffffff8411610ea5576040517fdb0000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f850000000000000000000000000000000000000000000000000000000000000060368201527fffffffffff00000000000000000000000000000000000000000000000000000060d886901b166037820152603c016109b1565b65ffffffffffff8411610f89576040517fdc0000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f860000000000000000000000000000000000000000000000000000000000000060368201527fffffffffffff000000000000000000000000000000000000000000000000000060d086901b166037820152603d016109b1565b66ffffffffffffff841161106e576040517fdd0000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f870000000000000000000000000000000000000000000000000000000000000060368201527fffffffffffffff0000000000000000000000000000000000000000000000000060c886901b166037820152603e016109b1565b6040517fde0000000000000000000000000000000000000000000000000000000000000060208201527fff00000000000000000000000000000000000000000000000000000000000000821660218201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606087901b1660228201527f880000000000000000000000000000000000000000000000000000000000000060368201527fffffffffffffffff00000000000000000000000000000000000000000000000060c086901b166037820152603f0160405160208183030381529060405291505b5080516020909101209392505050565b60006104d86111e260408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b83611505565b600061126f61126860408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b8484610539565b9392505050565b600061126f83837f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed6119ed565b60008451602086018451f090506112b9816123d3565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b51190600090a26000808273ffffffffffffffffffffffffffffffffffffffff168560200151876040516113279190612d29565b60006040518083038185875af1925050503d8060008114611364576040519150601f19603f3d011682016040523d82523d6000602084013e611369565b606091505b5091509150816113c9577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed816040517fa57ca23900000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed1631156114fb578373ffffffffffffffffffffffffffffffffffffffff167f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed73ffffffffffffffffffffffffffffffffffffffff163160405160006040518083038185875af1925050503d8060008114611495576040519150601f19603f3d011682016040523d82523d6000602084013e61149a565b606091505b509092509050816114fb577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed816040517fc2b3f44500000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b5050949350505050565b600080611511846120db565b905060006040518060400160405280601081526020017f67363d3d37363d34f03d5260086018f30000000000000000000000000000000081525090506000828251602084016000f5905073ffffffffffffffffffffffffffffffffffffffff81166115e0576040517fc05cee7a00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b604051839073ffffffffffffffffffffffffffffffffffffffff8316907f2feea65dd4e9f9cbd86b74b7734210c59a1b2981b5b137bd0ee3e208200c906790600090a361162c83610823565b935060008173ffffffffffffffffffffffffffffffffffffffff1634876040516116569190612d29565b60006040518083038185875af1925050503d8060008114611693576040519150601f19603f3d011682016040523d82523d6000602084013e611698565b606091505b505090506116a681866124ff565b60405173ffffffffffffffffffffffffffffffffffffffff8616907f4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b51190600090a25050505092915050565b6000806116fd876120db565b9050808651602088018651f59150611714826123d3565b604051819073ffffffffffffffffffffffffffffffffffffffff8416907fb8fda7e00c6b06a2b54e58521bc5894fee35f1090e5a3bb6390bfe2b98b497f790600090a36000808373ffffffffffffffffffffffffffffffffffffffff168660200151886040516117849190612d29565b60006040518083038185875af1925050503d80600081146117c1576040519150601f19603f3d011682016040523d82523d6000602084013e6117c6565b606091505b509150915081611826577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed816040517fa57ca23900000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed163115611958578473ffffffffffffffffffffffffffffffffffffffff167f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed73ffffffffffffffffffffffffffffffffffffffff163160405160006040518083038185875af1925050503d80600081146118f2576040519150601f19603f3d011682016040523d82523d6000602084013e6118f7565b606091505b50909250905081611958577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed816040517fc2b3f44500000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b50505095945050505050565b60006107b36119e460408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b858585336116f1565b6000604051836040820152846020820152828152600b8101905060ff815360559020949350505050565b600080611a23876120db565b905060006040518060400160405280601081526020017f67363d3d37363d34f03d5260086018f30000000000000000000000000000000081525090506000828251602084016000f5905073ffffffffffffffffffffffffffffffffffffffff8116611af2576040517fc05cee7a00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b604051839073ffffffffffffffffffffffffffffffffffffffff8316907f2feea65dd4e9f9cbd86b74b7734210c59a1b2981b5b137bd0ee3e208200c906790600090a3611b3e83610823565b935060008173ffffffffffffffffffffffffffffffffffffffff1687600001518a604051611b6c9190612d29565b60006040518083038185875af1925050503d8060008114611ba9576040519150601f19603f3d011682016040523d82523d6000602084013e611bae565b606091505b50509050611bbc81866124ff565b60405173ffffffffffffffffffffffffffffffffffffffff8616907f4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b51190600090a260608573ffffffffffffffffffffffffffffffffffffffff1688602001518a604051611c299190612d29565b60006040518083038185875af1925050503d8060008114611c66576040519150601f19603f3d011682016040523d82523d6000602084013e611c6b565b606091505b50909250905081611ccc577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed816040517fa57ca23900000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed163115611dfe578673ffffffffffffffffffffffffffffffffffffffff167f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed73ffffffffffffffffffffffffffffffffffffffff163160405160006040518083038185875af1925050503d8060008114611d98576040519150601f19603f3d011682016040523d82523d6000602084013e611d9d565b606091505b50909250905081611dfe577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed816040517fc2b3f44500000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b505050505095945050505050565b60006103dd611e8c60408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b868686866116f1565b60006103dd85858585336116f1565b60006103dd611f2460408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b86868686611a17565b6000808360601b90506040517f3d602d80600a3d3981f3363d3d373d3d3d363d7300000000000000000000000081528160148201527f5af43d82803e903d91602b57fd5bf3000000000000000000000000000000000060288201526037816000f092505073ffffffffffffffffffffffffffffffffffffffff8216612016576040517fc05cee7a00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b60405173ffffffffffffffffffffffffffffffffffffffff8316907f4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b51190600090a26000808373ffffffffffffffffffffffffffffffffffffffff1634866040516120809190612d29565b60006040518083038185875af1925050503d80600081146120bd576040519150601f19603f3d011682016040523d82523d6000602084013e6120c2565b606091505b50915091506120d282828861247d565b50505092915050565b60008060006120e9846125b3565b9092509050600082600281111561210257612102612e02565b1480156121205750600081600281111561211e5761211e612e02565b145b1561215e57604080513360208201524691810191909152606081018590526080016040516020818303038152906040528051906020012092506123cc565b600082600281111561217257612172612e02565b1480156121905750600181600281111561218e5761218e612e02565b145b156121b0576121a9338560009182526020526040902090565b92506123cc565b60008260028111156121c4576121c4612e02565b03612233576040517f13b3a2a100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b600182600281111561224757612247612e02565b1480156122655750600081600281111561226357612263612e02565b145b1561227e576121a9468560009182526020526040902090565b600182600281111561229257612292612e02565b1480156122b0575060028160028111156122ae576122ae612e02565b145b1561231f576040517f13b3a2a100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b61239a60408051437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101406020830152419282019290925260608101919091524260808201524460a08201524660c08201523360e08201526000906101000160405160208183030381529060405280519060200120905090565b84036123a657836123c9565b604080516020810186905201604051602081830303815290604052805190602001205b92505b5050919050565b73ffffffffffffffffffffffffffffffffffffffff8116158061240b575073ffffffffffffffffffffffffffffffffffffffff81163b155b1561247a576040517fc05cee7a00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b50565b82158061249f575073ffffffffffffffffffffffffffffffffffffffff81163b155b156124fa577f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed826040517fa57ca23900000000000000000000000000000000000000000000000000000000815260040161062c929190612d94565b505050565b811580612520575073ffffffffffffffffffffffffffffffffffffffff8116155b80612540575073ffffffffffffffffffffffffffffffffffffffff81163b155b156125af576040517fc05cee7a00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000ba5ed099633d3b313e4d5f7bdc1305d3c28ba5ed16600482015260240161062c565b5050565b600080606083901c3314801561261057508260141a60f81b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19167f0100000000000000000000000000000000000000000000000000000000000000145b1561262057506000905080915091565b606083901c3314801561265a57507fff00000000000000000000000000000000000000000000000000000000000000601484901a60f81b16155b1561266b5750600090506001915091565b33606084901c036126825750600090506002915091565b606083901c1580156126db57508260141a60f81b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19167f0100000000000000000000000000000000000000000000000000000000000000145b156126ec5750600190506000915091565b606083901c15801561272557507fff00000000000000000000000000000000000000000000000000000000000000601484901a60f81b16155b1561273557506001905080915091565b606083901c61274a5750600190506002915091565b8260141a60f81b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19167f0100000000000000000000000000000000000000000000000000000000000000036127a55750600290506000915091565b8260141a60f81b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166000036127e15750600290506001915091565b506002905080915091565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600082601f83011261282c57600080fd5b813567ffffffffffffffff80821115612847576128476127ec565b604051601f83017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190828211818310171561288d5761288d6127ec565b816040528381528660208588010111156128a657600080fd5b836020870160208301376000602085830101528094505050505092915050565b6000604082840312156128d857600080fd5b6040516040810181811067ffffffffffffffff821117156128fb576128fb6127ec565b604052823581526020928301359281019290925250919050565b60008060008060a0858703121561292b57600080fd5b84359350602085013567ffffffffffffffff8082111561294a57600080fd5b6129568883890161281b565b9450604087013591508082111561296c57600080fd5b506129798782880161281b565b92505061298986606087016128c6565b905092959194509250565b600080604083850312156129a757600080fd5b82359150602083013567ffffffffffffffff8111156129c557600080fd5b6129d18582860161281b565b9150509250929050565b6000602082840312156129ed57600080fd5b813567ffffffffffffffff811115612a0457600080fd5b6107b38482850161281b565b803573ffffffffffffffffffffffffffffffffffffffff81168114612a3457600080fd5b919050565b600080600060608486031215612a4e57600080fd5b83359250612a5e60208501612a10565b9150604084013567ffffffffffffffff811115612a7a57600080fd5b612a868682870161281b565b9150509250925092565b600060208284031215612aa257600080fd5b5035919050565b600080600060808486031215612abe57600080fd5b833567ffffffffffffffff80821115612ad657600080fd5b612ae28783880161281b565b94506020860135915080821115612af857600080fd5b50612b058682870161281b565b925050612b1585604086016128c6565b90509250925092565b60008060408385031215612b3157600080fd5b82359150612b4160208401612a10565b90509250929050565b60008060408385031215612b5d57600080fd5b612b6683612a10565b946020939093013593505050565b60008060408385031215612b8757600080fd5b612b9083612a10565b9150602083013567ffffffffffffffff8111156129c557600080fd5b60008060408385031215612bbf57600080fd5b50508035926020909101359150565b60008060008060a08587031215612be457600080fd5b843567ffffffffffffffff80821115612bfc57600080fd5b612c088883890161281b565b95506020870135915080821115612c1e57600080fd5b50612c2b8782880161281b565b935050612c3b86604087016128c6565b915061298960808601612a10565b600080600080600060c08688031215612c6157600080fd5b85359450602086013567ffffffffffffffff80821115612c8057600080fd5b612c8c89838a0161281b565b95506040880135915080821115612ca257600080fd5b50612caf8882890161281b565b935050612cbf87606088016128c6565b9150612ccd60a08701612a10565b90509295509295909350565b600080600060608486031215612cee57600080fd5b8335925060208401359150612b1560408501612a10565b60005b83811015612d20578181015183820152602001612d08565b50506000910152565b60008251612d3b818460208701612d05565b9190910192915050565b67ffffffffffffffff828116828216039080821115612d8d577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b5092915050565b73ffffffffffffffffffffffffffffffffffffffff831681526040602082015260008251806040840152612dcf816060850160208701612d05565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016060019392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fdfea164736f6c6343000817000a" +} diff --git a/packages/bridge-contracts/deploy/utils/oftSalts.ts b/packages/bridge-contracts/deploy/utils/oftSalts.ts new file mode 100644 index 0000000..d615d2a --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/oftSalts.ts @@ -0,0 +1,28 @@ +/*** + * Salt labels for the OFT proxies, shared by the deploy script and the `oft:addresses` task. + * + * The label is the only entropy behind the CREATE3 address, so these strings are effectively the + * deployment's identity: changing one moves the contract to a different address on every chain. + */ + +import { createXSalt } from './createX'; + +export function oftEnvPrefix(networkName: string): string { + return networkName.includes('development') ? 'Development' : 'Production'; +} + +export function oftSaltLabels(networkName: string): { minterBurner: string; adapter: string } { + const envPrefix = oftEnvPrefix(networkName); + return { + minterBurner: `${envPrefix}-GoodDollarOFTMinterBurnerV1`, + adapter: `${envPrefix}-GoodDollarOFTAdapterV1`, + }; +} + +export function oftProxySalts(networkName: string, deployer: string): { minterBurner: string; adapter: string } { + const labels = oftSaltLabels(networkName); + return { + minterBurner: createXSalt(deployer, labels.minterBurner), + adapter: createXSalt(deployer, labels.adapter), + }; +} diff --git a/packages/bridge-contracts/hardhat.config.ts b/packages/bridge-contracts/hardhat.config.ts index c441e50..4223810 100644 --- a/packages/bridge-contracts/hardhat.config.ts +++ b/packages/bridge-contracts/hardhat.config.ts @@ -17,6 +17,7 @@ import { EndpointId } from '@layerzerolabs/lz-definitions'; import './scripts/oft/configure-oft'; import './scripts/oft/verify-invariants'; +import './scripts/oft/predict-addresses'; configDotenv(); envEnc.config(); diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md index 79daab7..82cdb6e 100644 --- a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -7,6 +7,23 @@ From `packages/bridge-contracts`. Addresses are written to git-ignored hardhat-d ## Deploy +Everything is deployed through [CreateX](https://github.com/pcaversaccio/createx), the universal +deployer at `0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed` on every chain. The two proxies are +CREATE3-deployed, so **both get the same address on every chain** - the address comes from +(CreateX, deployer EOA, salt label) only, not from the implementation or the chain. + +Two consequences to respect: + +- **Use the same deployer key on every chain.** The deployer is part of the salt; a different signer + means a different address. Check before spending gas: + ```bash + npx hardhat oft:addresses --network production-xdc + npx hardhat oft:addresses --network production-celo + ``` + Both runs must print the same two addresses. +- Only the deployer EOA can use its own salt, so nobody can squat the address on a chain you have + not deployed to yet. + ```bash npx hardhat deploy --tags OFT --network production-xdc npx hardhat deploy --tags OFT --network production-celo @@ -14,14 +31,20 @@ npx hardhat deploy --tags OFT --network production-celo Use `development-xdc` / `development-celo` for staging. Deploy **both** chains before configuring (LayerZero wire needs both adapters, and must run while the deployer still owns the OApp). +Re-running the deploy is safe: anything already at its deterministic address is reused rather than redeployed. + ### Deploy parameters (`deploy/deployOFT.ts`) | Parameter | Source | Notes | |-----------|--------|--------| | Token / NameService / Controller | `@gooddollar/goodprotocol` `deployment.json` for the Hardhat network | Required | | LayerZero endpoint | `@layerzerolabs/lz-evm-sdk-v2` `EndpointV2.json` via `deploy/utils/getLzEndpoint.ts` (fallback: `LAYERZERO_ENDPOINT` if network unmapped) | Celo/XDC mainnet deployments | -| Proxy CREATE2 salt | `Development-…V1` or `Production-…V1` string hash | Fixed proxy address per env | -| Implementation CREATE2 salt | `keccak256(bytecode)` | Changes when code changes | +| Proxy CREATE3 salt | `bytes20(deployer) \| 0x00 \| bytes11(keccak256("Development-…V1" / "Production-…V1"))`, see `deploy/utils/oftSalts.ts` | Same proxy address on every chain, per env | +| Implementation CREATE2 salt | `bytes20(deployer) \| 0x00 \| bytes11(keccak256(initCode))` | Changes when code or constructor args change, so the adapter implementation is per-chain | + +The `0x00` flag byte is what keeps `block.chainid` out of CreateX's guarded salt; `0x01` there would +give a different address per chain. The proxy itself is GoodProtocol's `ERC1967Proxy`, deployed and +initialized in a single `deployCreate3AndInit` transaction. ## Configure @@ -74,7 +97,7 @@ Values are **G$ amounts** (e.g. `"100"` → 100 G$), not wei: ## Upgrade -Proxy addresses stay the same: +Proxy addresses stay the same; new implementations are CREATE2-deployed through CreateX from the same deployer: ```bash npx hardhat deploy --tags OFT-Upgrade --network production-xdc @@ -101,7 +124,7 @@ Suggested order: 1. **Hardhat** — add the network in `hardhat.config.ts` (RPC, accounts, and `eid: EndpointId._V2_…` like the existing celo/xdc entries) 2. **Deploy endpoint** — map Hardhat name → `@layerzerolabs/lz-evm-sdk-v2` `deployments//EndpointV2.json` in `getLzEndpoint.ts`, or set `LAYERZERO_ENDPOINT` for that run (required by the OFT adapter **constructor**, not by `lz:oapp:wire`) 3. **GoodProtocol** — ensure `deployment.json` (or local overrides) has GoodDollar / NameService / Controller / Avatar for that network (configure steps need them) -4. **Deploy** — `npx hardhat deploy --tags OFT --network ` → `deployments//GoodDollarOFTAdapter.json` +4. **Deploy** — check CreateX is deployed on the chain ([deployment list](https://github.com/pcaversaccio/createx#deployments); the deploy script refuses to run otherwise), confirm `npx hardhat oft:addresses --network ` prints the same addresses as the existing chains, then `npx hardhat deploy --tags OFT --network ` → `deployments//GoodDollarOFTAdapter.json` 5. **`layerzero.config.ts`** — add an `OmniPointHardhat` (eid + adapter from `getOftDeploymentAddresses`); add `pathways` for each pair you want (e.g. new↔CELO, new↔XDC). The current file is a single XDC↔CELO pathway driven by `OFT_XDC_NETWORK` / `OFT_CELO_NETWORK` — multi-chain needs that structure updated, not only one new pathway line 6. **Configure helpers** — `oft.config.json` limits; extend/refactor `OFT_NETWORK_PAIRS` in `configure-oft.ts` so `oft:configure` knows how to set peer env for wiring 7. **Wire then configure** — `yarn oft:configure --network ` (and on each peer). Wire runs **first** (deployer must still own the OApp); ownership transfer to Avatar is **last** diff --git a/packages/bridge-contracts/scripts/oft/predict-addresses.ts b/packages/bridge-contracts/scripts/oft/predict-addresses.ts new file mode 100644 index 0000000..7d18eb4 --- /dev/null +++ b/packages/bridge-contracts/scripts/oft/predict-addresses.ts @@ -0,0 +1,46 @@ +import { task } from 'hardhat/config'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { computeCreate3Address, getCreateX } from '../../deploy/utils/createX'; +import { oftProxySalts, oftSaltLabels } from '../../deploy/utils/oftSalts'; + +/** + * Prints the CREATE3 addresses the OFT proxies will get on this network, before any gas is spent. + * The addresses only depend on CreateX, the deployer EOA and the salt label, so running this on + * every target network must print the same two addresses - if it doesn't, the deployer differs. + */ +export async function predictOftAddresses(hre: HardhatRuntimeEnvironment, deployerOverride?: string) { + const { ethers, network } = hre; + const networkName = network.name; + + const deployer = deployerOverride + ? ethers.utils.getAddress(deployerOverride) + : await ethers.getSigners().then(([signer]) => signer.address); + + const createX = await getCreateX(); + const salts = oftProxySalts(networkName, deployer); + const labels = oftSaltLabels(networkName); + + const addresses = { + GoodDollarOFTMinterBurner: computeCreate3Address(deployer, salts.minterBurner), + GoodDollarOFTAdapter: computeCreate3Address(deployer, salts.adapter), + }; + + console.log(`\n=== Predicted OFT proxy addresses (${networkName}) ===`); + console.log('CreateX:', createX.address); + console.log('Deployer:', deployer); + for (const [name, address] of Object.entries(addresses)) { + const label = name === 'GoodDollarOFTAdapter' ? labels.adapter : labels.minterBurner; + const deployed = (await ethers.provider.getCode(address)) !== '0x'; + console.log(`${name}: ${address} (salt "${label}"${deployed ? ', already deployed' : ''})`); + } + console.log('These addresses are the same on every chain deployed from this signer.'); + console.log('=====================================\n'); + + return addresses; +} + +task('oft:addresses', 'Print the deterministic CreateX addresses the OFT proxies get on this network') + .addOptionalParam('deployer', 'Predict for this deployer instead of the configured signer') + .setAction(async (args, hre) => { + await predictOftAddresses(hre, args.deployer); + }); diff --git a/packages/bridge-contracts/scripts/oft/verify-invariants.ts b/packages/bridge-contracts/scripts/oft/verify-invariants.ts index 321969d..012719e 100644 --- a/packages/bridge-contracts/scripts/oft/verify-invariants.ts +++ b/packages/bridge-contracts/scripts/oft/verify-invariants.ts @@ -66,6 +66,13 @@ export async function verifyOftInvariants(hre: HardhatRuntimeEnvironment) { same(await adapter.peers(remoteEid), ethers.utils.hexZeroPad(remoteAdapter, 32)), `adapter.peers(${remoteEid}) == ${remoteNetwork} adapter ${remoteAdapter}` ); + // CREATE3 deployments from one deployer share an address across chains; a mismatch means the + // chains were deployed from different keys or salts. The bridge still works, so this only warns. + if (!same(adapterAddress, remoteAdapter)) { + console.log( + `warn adapter address differs from ${remoteNetwork}: ${adapterAddress} vs ${remoteAdapter}` + ); + } const token = await ethers.getContractAt( "contracts/oft/interfaces/ISuperGoodDollar.sol:ISuperGoodDollar", diff --git a/packages/bridge-contracts/test/CreateXDeploy.test.ts b/packages/bridge-contracts/test/CreateXDeploy.test.ts new file mode 100644 index 0000000..f323d35 --- /dev/null +++ b/packages/bridge-contracts/test/CreateXDeploy.test.ts @@ -0,0 +1,223 @@ +import { artifacts, ethers, network } from 'hardhat'; +import { expect } from 'chai'; +import { loadFixture, takeSnapshot } from '@nomicfoundation/hardhat-network-helpers'; +import { + CREATEX_ADDRESS, + CREATEX_RUNTIME_CODE_HASH, + computeCreate2Address, + computeCreate3Address, + createXSalt, + deployCreate2IfMissing, + deployCreate3AndInitIfMissing, + getCreateX, + guardCreateXSalt, +} from '../deploy/utils/createX'; +import { oftProxySalts, oftSaltLabels } from '../deploy/utils/oftSalts'; +import { ERC1967_PROXY_FQN } from '../deploy/deployOFT'; + +// The deploy scripts put the OFT proxies behind CreateX so that their address is derived from +// (CreateX, deployer, salt) only - identical on every chain, and independent of the implementation. +describe('CreateX deterministic deployment', () => { + async function fixture() { + const [deployer, other] = await ethers.getSigners(); + + // getCreateX etches the real CreateX runtime code at its canonical address on local networks + const createX = await getCreateX(deployer); + + const nameService = await (await ethers.getContractFactory('NameServiceMock')).deploy(); + const controller = await (await ethers.getContractFactory('ControllerMock')).deploy(other.address); + const token = await (await ethers.getContractFactory('MockGoodDollar')).deploy('GoodDollar', 'G$'); + await nameService.setAddress('CONTROLLER', controller.address); + await nameService.setAddress('GOODDOLLAR', token.address); + + const minterBurnerFactory = await ethers.getContractFactory('GoodDollarOFTMinterBurner'); + const minterBurnerInitCode = minterBurnerFactory.getDeployTransaction().data as string; + + const proxyArtifact = await artifacts.readArtifact(ERC1967_PROXY_FQN); + const proxyInterface = new ethers.utils.Interface(proxyArtifact.abi); + + return { + deployer, + other, + createX, + nameService, + token, + minterBurnerFactory, + minterBurnerInitCode, + proxyBytecode: proxyArtifact.bytecode, + proxyInterface, + }; + } + + describe('salt', () => { + it('is permissioned for the deployer and leaves out the chain id', async () => { + const { deployer } = await loadFixture(fixture); + const salt = createXSalt(deployer.address, 'Production-GoodDollarOFTAdapterV1'); + + expect(ethers.utils.hexDataLength(salt)).to.equal(32); + // bytes20(salt) == deployer makes CreateX reserve the resulting address for that deployer + expect(ethers.utils.getAddress(ethers.utils.hexDataSlice(salt, 0, 20))).to.equal(deployer.address); + // flag byte 0x00 == no cross-chain redeploy protection, so block.chainid stays out of the guarded salt + expect(ethers.utils.hexDataSlice(salt, 20, 21)).to.equal('0x00'); + // guarded salt is keccak256(bytes32(deployer) ++ salt): deployer and salt only, no chain id + expect(guardCreateXSalt(deployer.address, salt)).to.equal( + ethers.utils.keccak256(ethers.utils.hexConcat([ethers.utils.hexZeroPad(deployer.address, 32), salt])), + ); + }); + + it('gives production and development deployments different addresses', async () => { + const { deployer, createX } = await loadFixture(fixture); + const production = oftProxySalts('production-celo', deployer.address); + const development = oftProxySalts('development-celo', deployer.address); + + expect(oftSaltLabels('production-xdc')).to.deep.equal(oftSaltLabels('production-celo')); + expect(production.adapter).to.not.equal(development.adapter); + expect(computeCreate3Address(deployer.address, production.adapter)).to.not.equal( + computeCreate3Address(deployer.address, development.adapter), + ); + // our local derivation agrees with CreateX's own + expect(await createX.computeCreate3Address(guardCreateXSalt(deployer.address, production.adapter))).to.equal( + computeCreate3Address(deployer.address, production.adapter), + ); + }); + }); + + describe('CREATE3 proxy deployment', () => { + it('lands on the address predicted before the implementation exists', async () => { + const { + deployer, + createX, + nameService, + token, + minterBurnerFactory, + minterBurnerInitCode, + proxyBytecode, + proxyInterface, + } = await loadFixture(fixture); + const salt = oftProxySalts('production-celo', deployer.address).minterBurner; + + // predicted with nothing deployed yet - no implementation, no init data + const predicted = computeCreate3Address(deployer.address, salt); + expect(await ethers.provider.getCode(predicted)).to.equal('0x'); + + const impl = await deployCreate2IfMissing( + createX, + deployer.address, + createXSalt(deployer.address, ethers.utils.keccak256(minterBurnerInitCode)), + minterBurnerInitCode, + ); + const adapterPlaceholder = ethers.Wallet.createRandom().address; + const proxy = await deployCreate3AndInitIfMissing( + createX, + deployer.address, + salt, + proxyBytecode, + proxyInterface.encodeFunctionData('initialize', [ + impl.address, + minterBurnerFactory.interface.encodeFunctionData('initialize', [nameService.address, adapterPlaceholder]), + ]), + ); + + expect(proxy.address).to.equal(predicted); + expect(proxy.reused).to.equal(false); + + // deployed and initialized in a single transaction + const minterBurner = await ethers.getContractAt('GoodDollarOFTMinterBurner', proxy.address); + expect(await minterBurner.token()).to.equal(token.address); + expect(await minterBurner.operators(adapterPlaceholder)).to.equal(true); + const implSlot = await ethers.provider.getStorageAt( + proxy.address, + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', + ); + expect(ethers.utils.getAddress(ethers.utils.hexDataSlice(implSlot, 12))).to.equal(impl.address); + }); + + it('keeps the same address for a different implementation, unlike CREATE2', async () => { + const { deployer, createX, minterBurnerInitCode, proxyBytecode, proxyInterface } = await loadFixture(fixture); + const salt = oftProxySalts('production-celo', deployer.address).adapter; + + // a plain ERC1967 proxy as "other implementation": different init code, same salt + const otherInitCode = proxyBytecode; + expect(computeCreate2Address(deployer.address, salt, minterBurnerInitCode)).to.not.equal( + computeCreate2Address(deployer.address, salt, otherInitCode), + ); + // and CreateX agrees on the CREATE2 address we compute locally + expect( + await createX.computeCreate2Address( + guardCreateXSalt(deployer.address, salt), + ethers.utils.keccak256(minterBurnerInitCode), + ), + ).to.equal(computeCreate2Address(deployer.address, salt, minterBurnerInitCode)); + + // CREATE3 takes no init code at all, so the predicted address survives an implementation swap + const predicted = computeCreate3Address(deployer.address, salt); + const logic = await deployCreate2IfMissing( + createX, + deployer.address, + createXSalt(deployer.address, ethers.utils.keccak256(otherInitCode)), + otherInitCode, + ); + const proxy = await deployCreate3AndInitIfMissing( + createX, + deployer.address, + salt, + proxyBytecode, + proxyInterface.encodeFunctionData('initialize', [logic.address, '0x']), + ); + expect(proxy.address).to.equal(predicted); + }); + + it('is re-runnable: an existing deployment is reused instead of redeployed', async () => { + const { deployer, createX, minterBurnerInitCode, proxyBytecode, proxyInterface } = await loadFixture(fixture); + const implSalt = createXSalt(deployer.address, ethers.utils.keccak256(minterBurnerInitCode)); + const proxySalt = oftProxySalts('development-celo', deployer.address).adapter; + const initData = proxyInterface.encodeFunctionData('initialize', [ + (await deployCreate2IfMissing(createX, deployer.address, implSalt, minterBurnerInitCode)).address, + '0x', + ]); + + const implAgain = await deployCreate2IfMissing(createX, deployer.address, implSalt, minterBurnerInitCode); + expect(implAgain.reused).to.equal(true); + expect(implAgain.transactionHash).to.equal(undefined); + + const first = await deployCreate3AndInitIfMissing(createX, deployer.address, proxySalt, proxyBytecode, initData); + const second = await deployCreate3AndInitIfMissing(createX, deployer.address, proxySalt, proxyBytecode, initData); + expect(second.address).to.equal(first.address); + expect(second.reused).to.equal(true); + }); + + it('reserves the address for its deployer: another signer with the same salt lands elsewhere', async () => { + const { deployer, createX, other, proxyBytecode } = await loadFixture(fixture); + const salt = createXSalt(other.address, 'Production-GoodDollarOFTAdapterV1'); + const reserved = computeCreate3Address(other.address, salt); + + // a foreign signer passing the same salt gets the unpermissioned guarded salt, not ours + await (await createX.connect(deployer).deployCreate3(salt, proxyBytecode)).wait(); + expect(await ethers.provider.getCode(reserved)).to.equal('0x'); + + // the address is still there for the deployer the salt belongs to + await (await createX.connect(other).deployCreate3(salt, proxyBytecode)).wait(); + expect(await ethers.provider.getCode(reserved)).to.not.equal('0x'); + }); + }); + + describe('CreateX itself', () => { + it('matches the runtime code hash published by the CreateX project', async () => { + const { createX } = await loadFixture(fixture); + expect(createX.address).to.equal(CREATEX_ADDRESS); + expect(ethers.utils.keccak256(await ethers.provider.getCode(CREATEX_ADDRESS))).to.equal( + CREATEX_RUNTIME_CODE_HASH, + ); + }); + + it('refuses to deploy against foreign code at the CreateX address', async () => { + await loadFixture(fixture); + const snapshot = await takeSnapshot(); + await network.provider.send('hardhat_setCode', [CREATEX_ADDRESS, '0x6001600101']); + + await expect(getCreateX()).to.be.rejectedWith(/is not CreateX/); + + await snapshot.restore(); + }); + }); +}); diff --git a/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts b/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts index d74c573..7e63d59 100644 --- a/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts +++ b/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts @@ -1,4 +1,4 @@ -import { ethers, network, upgrades } from 'hardhat'; +import { artifacts, ethers, network, upgrades } from 'hardhat'; import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { GoodDollarOFTAdapter } from '../typechain-types'; @@ -6,6 +6,15 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import contracts from '@gooddollar/goodprotocol/releases/deployment.json'; import * as networkHelpers from "@nomicfoundation/hardhat-network-helpers"; import { deployMinterBurnerProxy } from './helpers/deployMinterBurnerProxy'; +import { + computeCreate3Address, + createXSalt, + guardCreateXSalt, + deployCreate2IfMissing, + deployCreate3AndInitIfMissing, + getCreateX, +} from '../deploy/utils/createX'; +import { ERC1967_PROXY_FQN } from '../deploy/deployOFT'; const CELO_CHAIN_URL = 'https://forno.celo.org'; /** @@ -430,49 +439,86 @@ describe('GoodDollarOFTAdapter Fork Tests (Celo Production)', () => { }); }); - // Mirrors deploy/deployOFT.ts + scripts/oft/steps/adapter-ownership.ts against the real Celo ProxyFactory/endpoint - describe('Atomic deploy via ProxyFactory1967 and Avatar handover (H-01/H-02)', () => { + // Mirrors deploy/deployOFT.ts + scripts/oft/steps/adapter-ownership.ts against the real Celo CreateX/endpoint + describe('Atomic deploy via CreateX and Avatar handover (H-01/H-02)', () => { it('deploys both proxies initialized in one tx each and hands owner + LZ delegate to the Avatar', async function () { const avatar = celoContracts.Avatar; - const proxyFactory = await ethers.getContractAt( - [ - 'function deployProxy(uint256 _salt, address _logic, bytes _data) returns (address)', - 'function getDeploymentAddress(uint256 _salt, address _sender) view returns (address)', - ], - celoContracts.ProxyFactory, - ); + // the real CreateX deployment on Celo, not an etched copy + const createX = await getCreateX(owner); const endpoint = await ethers.getContractAt( ['function delegates(address) view returns (address)'], CELO_LZ_ENDPOINT, ); - const minterBurnerImpl = await (await ethers.getContractFactory('GoodDollarOFTMinterBurner')).deploy(); + const MinterBurnerFactory = await ethers.getContractFactory('GoodDollarOFTMinterBurner'); const AdapterFactory = await ethers.getContractFactory('GoodDollarOFTAdapter'); - const adapterImpl = await AdapterFactory.deploy(celoContracts.GoodDollar, CELO_LZ_ENDPOINT); + const mbImplInitCode = MinterBurnerFactory.getDeployTransaction().data as string; + const adapterImplInitCode = AdapterFactory.getDeployTransaction( + celoContracts.GoodDollar, + CELO_LZ_ENDPOINT, + ).data as string; + const minterBurnerImpl = await deployCreate2IfMissing( + createX, + owner.address, + createXSalt(owner.address, ethers.utils.keccak256(mbImplInitCode)), + mbImplInitCode, + ); + const adapterImpl = await deployCreate2IfMissing( + createX, + owner.address, + createXSalt(owner.address, ethers.utils.keccak256(adapterImplInitCode)), + adapterImplInitCode, + ); - const mbSalt = ethers.BigNumber.from(ethers.utils.id('Test-GoodDollarOFTMinterBurnerV1')); - const adapterSalt = ethers.BigNumber.from(ethers.utils.id('Test-GoodDollarOFTAdapterV1')); - const mbAddress = await proxyFactory.getDeploymentAddress(mbSalt, owner.address); - const adapterAddress = await proxyFactory.getDeploymentAddress(adapterSalt, owner.address); + const mbSalt = createXSalt(owner.address, 'Test-GoodDollarOFTMinterBurnerV1'); + const adapterSalt = createXSalt(owner.address, 'Test-GoodDollarOFTAdapterV1'); + const mbAddress = computeCreate3Address(owner.address, mbSalt); + const adapterAddress = computeCreate3Address(owner.address, adapterSalt); + // the locally computed address is what the real CreateX derives on this chain + expect(await createX.computeCreate3Address(guardCreateXSalt(owner.address, adapterSalt))).to.equal( + adapterAddress, + ); // salt is bound to the deployer, so another sender gets a different address - expect(await proxyFactory.getDeploymentAddress(adapterSalt, user.address)).to.not.equal(adapterAddress); - - await proxyFactory.deployProxy( + expect( + computeCreate3Address(user.address, createXSalt(user.address, 'Test-GoodDollarOFTAdapterV1')), + ).to.not.equal(adapterAddress); + + const proxyArtifact = await artifacts.readArtifact(ERC1967_PROXY_FQN); + const proxyInterface = new ethers.utils.Interface(proxyArtifact.abi); + const mbProxy = await deployCreate3AndInitIfMissing( + createX, + owner.address, mbSalt, - minterBurnerImpl.address, - minterBurnerImpl.interface.encodeFunctionData('initialize', [celoContracts.NameService, adapterAddress]), + proxyArtifact.bytecode, + proxyInterface.encodeFunctionData('initialize', [ + minterBurnerImpl.address, + MinterBurnerFactory.interface.encodeFunctionData('initialize', [ + celoContracts.NameService, + adapterAddress, + ]), + ]), ); - await proxyFactory.deployProxy( + const adapterProxy = await deployCreate3AndInitIfMissing( + createX, + owner.address, adapterSalt, - adapterImpl.address, - AdapterFactory.interface.encodeFunctionData('initialize', [ - celoContracts.GoodDollar, - mbAddress, - owner.address, - avatar, + proxyArtifact.bytecode, + proxyInterface.encodeFunctionData('initialize', [ + adapterImpl.address, + AdapterFactory.interface.encodeFunctionData('initialize', [ + celoContracts.GoodDollar, + mbAddress, + owner.address, + avatar, + ]), ]), ); + // CREATE3: both proxies land on the addresses predicted before the implementations existed, + // and those addresses carry over to every other chain deployed from this signer + expect(mbProxy.address).to.equal(mbAddress); + expect(adapterProxy.address).to.equal(adapterAddress); + const implSlot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; const implOf = async (proxy: string) => ethers.utils.getAddress(ethers.utils.hexDataSlice(await ethers.provider.getStorageAt(proxy, implSlot), 12));