From 5d3c153baacfe6ef8edfce07b20bbaa06ebf9b27 Mon Sep 17 00:00:00 2001 From: ocakciomursel-pixel <276599321+ocakciomursel-pixel@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:50:15 +0300 Subject: [PATCH 1/2] feat(oft): deploy OFT proxies via CreateX CREATE3 for cross-chain deterministic addresses --- packages/bridge-contracts/deploy/deployOFT.ts | 49 +++++++------- .../bridge-contracts/deploy/utils/createX.ts | 62 +++++++++++++++++ .../scripts/oft/OFT_CONFIGURING_GUIDE.md | 2 +- .../test/GoodDollarOFT.e2e.test.ts | 66 +++++++++++++------ .../test/GoodDollarOFT.test.ts | 60 +++++++++++++++++ 5 files changed, 194 insertions(+), 45 deletions(-) create mode 100644 packages/bridge-contracts/deploy/utils/createX.ts diff --git a/packages/bridge-contracts/deploy/deployOFT.ts b/packages/bridge-contracts/deploy/deployOFT.ts index c35a8bd..d192086 100644 --- a/packages/bridge-contracts/deploy/deployOFT.ts +++ b/packages/bridge-contracts/deploy/deployOFT.ts @@ -7,23 +7,24 @@ * * 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) + * 2. Predict both proxy addresses via CreateX.computeCreate3Address(guardedSalt) + * 3. Deploy + initialize each proxy atomically via CreateX.deployCreate3AndInit(salt, proxyBytecode, initCallData, values) * - 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: CreateX is deployed at 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed across all chains, + * so sender-guarded CREATE3 salts produce identical deterministic proxy addresses on every chain. */ 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 { CREATEX_ABI, CREATEX_ADDRESS, buildCreateXSalt, guardCreateXSalt } from './utils/createX'; const func: DeployFunction = async function (hre) { const { deployments, network } = hre; @@ -125,35 +126,37 @@ const func: DeployFunction = async function (hre) { }); 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}`); + // CreateX deploys and initializes ERC1967Proxy in one tx via CREATE3, with a salt bound to the deployer + // (no init front-running) and cross-chain protection disabled (0x00 flag) so proxy addresses match across chains. + if ((await ethers.provider.getCode(CREATEX_ADDRESS)) === '0x') { + throw new Error(`CreateX not deployed at ${CREATEX_ADDRESS} on 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, + const createX = await ethers.getContractAt(CREATEX_ABI, CREATEX_ADDRESS); + const proxyArtifact = await hre.artifacts.readArtifact( + '@gooddollar/goodprotocol/contracts/utils/ProxyFactory1967.sol:ERC1967Proxy', ); + const proxyInterface = new ethers.utils.Interface(proxyArtifact.abi); const envPrefix = isDevelopment ? 'Development' : 'Production'; - const minterBurnerProxySalt = ethers.BigNumber.from( - ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`${envPrefix}-GoodDollarOFTMinterBurnerV1`)), + const minterBurnerProxySalt = buildCreateXSalt(root.address, `${envPrefix}-GoodDollarOFTMinterBurnerV1`); + const oftAdapterProxySalt = buildCreateXSalt(root.address, `${envPrefix}-GoodDollarOFTAdapterV1`); + // Both proxy addresses are known before deployment, which breaks the MinterBurner <-> adapter init cycle + const minterBurnerAddress: string = await createX['computeCreate3Address(bytes32)']( + guardCreateXSalt(root.address, minterBurnerProxySalt), ); - const oftAdapterProxySalt = ethers.BigNumber.from( - ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`${envPrefix}-GoodDollarOFTAdapterV1`)), + const oftAdapterAddress: string = await createX['computeCreate3Address(bytes32)']( + guardCreateXSalt(root.address, oftAdapterProxySalt), ); - // 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) => { + const deployProxyIfMissing = async (name: string, address: string, salt: string, 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); + const proxyInitCall = proxyInterface.encodeFunctionData('initialize', [logic, initData]); + const tx = await createX.deployCreate3AndInit(salt, proxyArtifact.bytecode, proxyInitCall, { + constructorAmount: 0, + initCallAmount: 0, + }); await tx.wait(); console.log(`${name} proxy deployed and initialized`, address, tx.hash); } diff --git a/packages/bridge-contracts/deploy/utils/createX.ts b/packages/bridge-contracts/deploy/utils/createX.ts new file mode 100644 index 0000000..7e03dfe --- /dev/null +++ b/packages/bridge-contracts/deploy/utils/createX.ts @@ -0,0 +1,62 @@ +import { ethers } from 'hardhat'; + +/** + * Canonical CreateX universal factory address deployed across EVM chains (Celo, XDC, Ethereum, Fuse, etc.). + * @see https://github.com/pcaversaccio/createx + */ +export const CREATEX_ADDRESS = '0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed'; + +/** + * Keccak-256 hash of CreateX's 16-byte CREATE3 child proxy initcode (`0x67363d3d37363d34f03d5260086018f3`). + */ +export const CREATE3_PROXY_INITCODE_HASH = '0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f'; + +export const CREATEX_ABI = [ + 'function deployCreate3AndInit(bytes32 salt, bytes initCode, bytes data, tuple(uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', + 'function computeCreate3Address(bytes32 salt) view returns (address computedAddress)', + 'function computeCreate3Address(bytes32 salt, address deployer) pure returns (address computedAddress)', +]; + +/** + * Builds a 32-byte CreateX salt with sender-permissioned deploy protection and cross-chain address consistency: + * - bytes 0..19 (20 bytes): `deployer` address (`SenderBytes.MsgSender` — only `deployer` can produce the guarded salt) + * - byte 20 (1 byte): `0x00` (`RedeployProtectionFlag.False` — omits `block.chainid` so the address is identical across all chains) + * - bytes 21..31 (11 bytes): first 11 bytes of `keccak256(utf8(label))` + */ +export function buildCreateXSalt(deployer: string, label: string): string { + const entropy = ethers.utils.hexDataSlice(ethers.utils.id(label), 0, 11); + return ethers.utils.solidityPack(['address', 'uint8', 'bytes11'], [ethers.utils.getAddress(deployer), 0, entropy]); +} + +/** + * Computes the post-`_guard(salt)` value that CreateX derives when `msg.sender == deployer` and `salt[20] == 0x00`: + * `_efficientHash(bytes32(uint256(uint160(msg.sender))), salt)` == `keccak256(abi.encode(deployer, salt))`. + */ +export function guardCreateXSalt(deployer: string, salt: string): string { + const normalizedDeployer = ethers.utils.getAddress(deployer); + const saltPrefix = ethers.utils.getAddress(ethers.utils.hexDataSlice(salt, 0, 20)); + if (saltPrefix !== normalizedDeployer) { + throw new Error(`CreateX salt sender prefix mismatch: expected ${normalizedDeployer}, got ${saltPrefix}`); + } + const redeployFlag = ethers.utils.hexDataSlice(salt, 20, 21); + if (redeployFlag !== '0x00') { + throw new Error(`CreateX cross-chain redeploy protection must be 0x00 for cross-chain deterministic addresses, got ${redeployFlag}`); + } + return ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode(['address', 'bytes32'], [normalizedDeployer, salt]), + ); +} + +/** + * Deterministically computes the CREATE3 deployment address for a sender-guarded CreateX salt. + * Depends only on `createXAddress`, `deployer`, and `salt` (independent of chain ID and contract bytecode). + */ +export function computeCreateXCreate3Address( + deployer: string, + salt: string, + createXAddress: string = CREATEX_ADDRESS, +): string { + const guardedSalt = guardCreateXSalt(deployer, salt); + const proxyChild = ethers.utils.getCreate2Address(createXAddress, guardedSalt, CREATE3_PROXY_INITCODE_HASH); + return ethers.utils.getContractAddress({ from: proxyChild, nonce: 1 }); +} diff --git a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md index 79daab7..30d4002 100644 --- a/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md +++ b/packages/bridge-contracts/scripts/oft/OFT_CONFIGURING_GUIDE.md @@ -20,7 +20,7 @@ Use `development-xdc` / `development-celo` for staging. Deploy **both** chains b |-----------|--------|--------| | 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 | +| Proxy CREATE3 salt | CreateX (`0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed`) sender-guarded salt (`00<11-byte hash>` of `Development-…V1` or `Production-…V1`) | Identical deterministic proxy address across all chains per env | | Implementation CREATE2 salt | `keccak256(bytecode)` | Changes when code changes | ## Configure diff --git a/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts b/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts index d74c573..3dfff1c 100644 --- a/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts +++ b/packages/bridge-contracts/test/GoodDollarOFT.e2e.test.ts @@ -6,6 +6,13 @@ 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 { + CREATEX_ABI, + CREATEX_ADDRESS, + buildCreateXSalt, + computeCreateXCreate3Address, + guardCreateXSalt, +} from '../deploy/utils/createX'; const CELO_CHAIN_URL = 'https://forno.celo.org'; /** @@ -430,17 +437,11 @@ 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)', () => { - it('deploys both proxies initialized in one tx each and hands owner + LZ delegate to the Avatar', async function () { + // 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 via CreateX 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, - ); + const createX = await ethers.getContractAt(CREATEX_ABI, CREATEX_ADDRESS); const endpoint = await ethers.getContractAt( ['function delegates(address) view returns (address)'], CELO_LZ_ENDPOINT, @@ -449,21 +450,40 @@ describe('GoodDollarOFTAdapter Fork Tests (Celo Production)', () => { const minterBurnerImpl = await (await ethers.getContractFactory('GoodDollarOFTMinterBurner')).deploy(); const AdapterFactory = await ethers.getContractFactory('GoodDollarOFTAdapter'); const adapterImpl = await AdapterFactory.deploy(celoContracts.GoodDollar, CELO_LZ_ENDPOINT); + const proxyFactory = await ethers.getContractFactory( + '@gooddollar/goodprotocol/contracts/utils/ProxyFactory1967.sol:ERC1967Proxy', + ); - 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 = buildCreateXSalt(owner.address, 'Test-GoodDollarOFTMinterBurnerV1'); + const adapterSalt = buildCreateXSalt(owner.address, 'Test-GoodDollarOFTAdapterV1'); + const mbAddress = await createX['computeCreate3Address(bytes32)'](guardCreateXSalt(owner.address, mbSalt)); + const adapterAddress = await createX['computeCreate3Address(bytes32)']( + guardCreateXSalt(owner.address, adapterSalt), + ); + expect(mbAddress).to.equal(computeCreateXCreate3Address(owner.address, mbSalt)); + expect(adapterAddress).to.equal(computeCreateXCreate3Address(owner.address, adapterSalt)); // salt is bound to the deployer, so another sender gets a different address - expect(await proxyFactory.getDeploymentAddress(adapterSalt, user.address)).to.not.equal(adapterAddress); + const otherSenderSalt = buildCreateXSalt(user.address, 'Test-GoodDollarOFTAdapterV1'); + expect(await createX['computeCreate3Address(bytes32)'](guardCreateXSalt(user.address, otherSenderSalt))).to.not.equal( + adapterAddress, + ); - await proxyFactory.deployProxy( - mbSalt, + // Even if another caller passes the deployer's raw salt, CreateX guards it to a different address + const mbInitCall = proxyFactory.interface.encodeFunctionData('initialize', [ minterBurnerImpl.address, minterBurnerImpl.interface.encodeFunctionData('initialize', [celoContracts.NameService, adapterAddress]), - ); - await proxyFactory.deployProxy( - adapterSalt, + ]); + await createX.connect(user).deployCreate3AndInit(mbSalt, proxyFactory.bytecode, mbInitCall, { + constructorAmount: 0, + initCallAmount: 0, + }); + expect(await ethers.provider.getCode(mbAddress)).to.equal('0x'); + + await createX.deployCreate3AndInit(mbSalt, proxyFactory.bytecode, mbInitCall, { + constructorAmount: 0, + initCallAmount: 0, + }); + const adapterInitCall = proxyFactory.interface.encodeFunctionData('initialize', [ adapterImpl.address, AdapterFactory.interface.encodeFunctionData('initialize', [ celoContracts.GoodDollar, @@ -471,7 +491,11 @@ describe('GoodDollarOFTAdapter Fork Tests (Celo Production)', () => { owner.address, avatar, ]), - ); + ]); + await createX.deployCreate3AndInit(adapterSalt, proxyFactory.bytecode, adapterInitCall, { + constructorAmount: 0, + initCallAmount: 0, + }); const implSlot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; const implOf = async (proxy: string) => diff --git a/packages/bridge-contracts/test/GoodDollarOFT.test.ts b/packages/bridge-contracts/test/GoodDollarOFT.test.ts index 7772d4a..5618c55 100644 --- a/packages/bridge-contracts/test/GoodDollarOFT.test.ts +++ b/packages/bridge-contracts/test/GoodDollarOFT.test.ts @@ -2,6 +2,12 @@ import { ethers, upgrades } from "hardhat"; import { expect } from "chai"; import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; import { deployMinterBurnerProxy } from "./helpers/deployMinterBurnerProxy"; +import { + CREATEX_ADDRESS, + buildCreateXSalt, + computeCreateXCreate3Address, + guardCreateXSalt, +} from "../deploy/utils/createX"; // OFT message encoding helpers (mirrors OFTMsgCodec library) // message = abi.encodePacked(bytes32(to), uint64(amountSD)) @@ -1154,4 +1160,58 @@ describe("OFT (unit, no fork)", () => { expect(await token.balanceOf(user.address)).to.equal(amount); }); }); + + describe("CreateX deterministic address helpers", () => { + it("encodes deployer prefix, 0x00 cross-chain flag, and 11-byte label hash in buildCreateXSalt", async () => { + const [deployer] = await ethers.getSigners(); + const label = "GoodDollarOFTAdapter_v1"; + const salt = buildCreateXSalt(deployer.address, label); + + expect(ethers.utils.hexDataLength(salt)).to.equal(32); + expect(ethers.utils.getAddress(ethers.utils.hexDataSlice(salt, 0, 20))).to.equal(deployer.address); + expect(ethers.utils.hexDataSlice(salt, 20, 21)).to.equal("0x00"); + expect(ethers.utils.hexDataSlice(salt, 21, 32)).to.equal( + ethers.utils.hexDataSlice(ethers.utils.id(label), 0, 11) + ); + }); + + it("guards sender-bound salts identically to CreateX._guard and rejects mismatched callers or chainId flags", async () => { + const [deployer, other] = await ethers.getSigners(); + const salt = buildCreateXSalt(deployer.address, "GoodDollarOFTAdapter_v1"); + + const guarded = guardCreateXSalt(deployer.address, salt); + expect(guarded).to.equal( + ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["address", "bytes32"], [deployer.address, salt])) + ); + + expect(() => guardCreateXSalt(other.address, salt)).to.throw("CreateX salt sender prefix mismatch"); + + const chainGuardedSalt = ethers.utils.hexConcat([ + ethers.utils.hexDataSlice(salt, 0, 20), + "0x01", + ethers.utils.hexDataSlice(salt, 21, 32), + ]); + expect(() => guardCreateXSalt(deployer.address, chainGuardedSalt)).to.throw( + "CreateX cross-chain redeploy protection must be 0x00" + ); + }); + + it("computes deterministic CREATE3 addresses that match across chains for the same deployer and differ per label/deployer", async () => { + const [deployer, other] = await ethers.getSigners(); + const adapterSalt = buildCreateXSalt(deployer.address, "GoodDollarOFTAdapter_v1"); + const mbSalt = buildCreateXSalt(deployer.address, "GoodDollarOFTMinterBurner_v1"); + const otherAdapterSalt = buildCreateXSalt(other.address, "GoodDollarOFTAdapter_v1"); + + const adapterAddr = computeCreateXCreate3Address(deployer.address, adapterSalt); + const mbAddr = computeCreateXCreate3Address(deployer.address, mbSalt); + const otherAdapterAddr = computeCreateXCreate3Address(other.address, otherAdapterSalt); + + expect(ethers.utils.isAddress(adapterAddr)).to.equal(true); + expect(adapterAddr).to.not.equal(mbAddr); + expect(adapterAddr).to.not.equal(otherAdapterAddr); + // Repeating with explicit CREATEX_ADDRESS yields identical address regardless of chain + expect(computeCreateXCreate3Address(deployer.address, adapterSalt, CREATEX_ADDRESS)).to.equal(adapterAddr); + }); + }); }); + From a34e98672160461b61b93efdfc48103a30a49ec9 Mon Sep 17 00:00:00 2001 From: ocakciomursel-pixel Date: Sun, 18 Oct 2026 12:03:10 +0300 Subject: [PATCH 2/2] fix(bridge-contracts): handle concurrent CreateX proxy deploy race and clarify post-guard salt --- packages/bridge-contracts/deploy/deployOFT.ts | 23 +++++++++++++------ .../bridge-contracts/deploy/utils/createX.ts | 7 +++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/bridge-contracts/deploy/deployOFT.ts b/packages/bridge-contracts/deploy/deployOFT.ts index d192086..b8d3728 100644 --- a/packages/bridge-contracts/deploy/deployOFT.ts +++ b/packages/bridge-contracts/deploy/deployOFT.ts @@ -140,7 +140,9 @@ const func: DeployFunction = async function (hre) { const envPrefix = isDevelopment ? 'Development' : 'Production'; const minterBurnerProxySalt = buildCreateXSalt(root.address, `${envPrefix}-GoodDollarOFTMinterBurnerV1`); const oftAdapterProxySalt = buildCreateXSalt(root.address, `${envPrefix}-GoodDollarOFTAdapterV1`); - // Both proxy addresses are known before deployment, which breaks the MinterBurner <-> adapter init cycle + // Both proxy addresses are known before deployment, which breaks the MinterBurner <-> adapter init cycle. + // Note: CreateX.computeCreate3Address(bytes32) does NOT run _guard(salt) internally (unlike deployCreate3AndInit), + // so it requires the post-guard salt from guardCreateXSalt(root.address, rawSalt). const minterBurnerAddress: string = await createX['computeCreate3Address(bytes32)']( guardCreateXSalt(root.address, minterBurnerProxySalt), ); @@ -153,12 +155,19 @@ const func: DeployFunction = async function (hre) { console.log(`${name} proxy already deployed`, address); } else { const proxyInitCall = proxyInterface.encodeFunctionData('initialize', [logic, initData]); - const tx = await createX.deployCreate3AndInit(salt, proxyArtifact.bytecode, proxyInitCall, { - constructorAmount: 0, - initCallAmount: 0, - }); - await tx.wait(); - console.log(`${name} proxy deployed and initialized`, address, tx.hash); + try { + const tx = await createX.deployCreate3AndInit(salt, proxyArtifact.bytecode, proxyInitCall, { + constructorAmount: 0, + initCallAmount: 0, + }); + await tx.wait(); + console.log(`${name} proxy deployed and initialized`, address, tx.hash); + } catch (err) { + if ((await ethers.provider.getCode(address)) === '0x') { + throw err; + } + console.log(`${name} proxy was deployed concurrently at ${address}; verifying state via verifyOftDeployment`); + } } await deployments.save(name, { address, abi: (await hre.artifacts.readArtifact(name)).abi }); }; diff --git a/packages/bridge-contracts/deploy/utils/createX.ts b/packages/bridge-contracts/deploy/utils/createX.ts index 7e03dfe..19b8138 100644 --- a/packages/bridge-contracts/deploy/utils/createX.ts +++ b/packages/bridge-contracts/deploy/utils/createX.ts @@ -13,6 +13,8 @@ export const CREATE3_PROXY_INITCODE_HASH = '0x21c35dbe1b344a2488cf3321d6ce542f8e export const CREATEX_ABI = [ 'function deployCreate3AndInit(bytes32 salt, bytes initCode, bytes data, tuple(uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', + // Note: CreateX's on-chain computeCreate3Address(bytes32) does NOT run _guard(salt); it expects the post-guard salt + // produced by guardCreateXSalt(deployer, rawSalt), whereas deployCreate3AndInit takes the raw salt and calls _guard(salt). 'function computeCreate3Address(bytes32 salt) view returns (address computedAddress)', 'function computeCreate3Address(bytes32 salt, address deployer) pure returns (address computedAddress)', ]; @@ -29,8 +31,11 @@ export function buildCreateXSalt(deployer: string, label: string): string { } /** - * Computes the post-`_guard(salt)` value that CreateX derives when `msg.sender == deployer` and `salt[20] == 0x00`: + * Computes the post-`_guard(salt)` value that CreateX derives internally in `deployCreate3*` when + * `msg.sender == deployer` and `salt[20] == 0x00`: * `_efficientHash(bytes32(uint256(uint160(msg.sender))), salt)` == `keccak256(abi.encode(deployer, salt))`. + * Note: CreateX's view function `computeCreate3Address(bytes32)` does NOT apply `_guard`, so callers of + * `computeCreate3Address(bytes32)` must pass this guarded salt, while `deployCreate3AndInit` takes the raw salt. */ export function guardCreateXSalt(deployer: string, salt: string): string { const normalizedDeployer = ethers.utils.getAddress(deployer);