Support for `create2` deploy without `createX` contract
Describe the feature
Add support for create2 factories, beyond createX.
Migration blocker for Safe (there might be a workaround).
Search terms
No response
Thanks for this suggestion - we want to provide pluggability in Ignition that will allow for swapping out logic like which create2 factory is used. But I don't have a timeline for it.
If people could add their interest to this ticket that would let us prioritize better.
Safe uses our own CREATE2 deployment factory and (at least last time I evaluated Ignition) was the major blocker keeping us from moving away from hardhat-deploy for contract deployments.
@nlordell if I understand correctly, you have your own deployment factory. Is that correct? You could deploy it and use functions that receive its future and the module builder to use it and return new futures.
Pros:
- You can do it today
Cons:
- You can't easily switch between EOA deployments and that, as it requires modifying your modules.
BTW, once Hardhat 3 is out, we want to evaluate integrating Safe more deeply.
Is that correct?
Yes.
You could deploy it and use functions that receive its future and the module builder to use it and return new futures.
I think I understand. I assume that I can build futures to:
- Send a pre-signed transaction if some contract is not already deployed (i.e. deploy the CREATE2 factory with a presigned transaction)
- Encoding the deployment of a contract by calling the CREATE2 factory - this should hopefully return a contract
Future, so I can interact with it just I can with them.contract('...')result
that receive its future and the module builder to use it and return new futures
A couple problems I ran into right away were:
- "what is the correct way to get init code?" for the contract I want to deploy. Looking at the
ModuleBuilderinterface, there is no obvious way to do it from there. You can importartficats/.../*.jsonbut that feels very hacky - "how do you encode the
CREATE2factory transaction?", you would have to create a future that encodes them.getParams(...)constructor arguments into the deployment bytecode, and I didn't find any documentation on how to do that (I poked around in theinternal/modules so I think it may be possible to do, but not in a way with a stable API AFAICT).
Anyway, here is my (naive) code (with point 2 unsolved):
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
import { ethers } from "ethers";
import Lock from "../../artifacts/contracts/Lock.sol/Lock.json";
const JAN_1ST_2030 = 1893456000;
const ONE_GWEI = 1_000_000_000n;
const SALT = ethers.ZeroHash;
const LOCK = new ethers.Interface(Lock.abi);
const FACTORY = "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7";
function getCreation(unlockTime: bigint) {
const code = ethers.concat([
Lock.bytecode,
LOCK.encodeDeploy([unlockTime]),
]);
const address = ethers.getCreate2Address(FACTORY, SALT, ethers.keccak256(code));
const data = ethers.concat([SALT, code]);
return { address, data };
}
const LockModule = buildModule("LockModule", (m) => {
const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030);
const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI);
// not sure how to make this a future that I can use...
const { address, data } = getCreation(BigInt(JAN_1ST_2030));
const deploy = m.send("deploy_Lock", FACTORY, 0n, data, {
value: lockedAmount,
});
const lock = m.contractAt("Lock", address);
return { deploy, lock };
});
export default LockModule;
@alcuadrado - out of curiosity, did you get a chance to look at the issue?
is there any way to create a send future with manually encoded data (instead of only Solidity ABI encoded data) that itself comes from a future? In pseudo-code what I want to achieve for the CREATE2 dployment (based on my understanding of Ignition):
const salt = m.getParameter("salt", DEFAULT_VALUE);
const artifact = m.???("MyContract"); // this is the artifact future for `MyContract`
const args = m.???([m.getParameter("constructor-arg1", ...), m.getParameter("constructor-arg2", ...)]); // the `abi.encode(args1, args2)`.
const creationData = m.???(salt, artifact, args) // `creationData` is a future for `abi.encodePacked(salt, artifact.bytecode, args)`
const send = m.send("create-my-contract", CREATE2_DEPLOYER, 0, creationData); // not currently possilble, as `data` as a future only accepts `EncodeFunctionCallFuture`, which can only encode Solidity ABI functions
const address = m.???(CREATE2_DEPLOYER, salt, artifact, args) // future that computes the CREATE2 address, based on the `salt`, `artifact`, and `args` parameter futures.
const myContract = m.contractAt("MyContract", artifact, address);
As a side question - it would be really nice if futures behaved as (zip-able) functors, so you can arbitrarily combine and map futures with pure transformations. This would allow me to trivially create custom futures for:
abi.encodePackedthe salt and bytecode used for the creationcallabi.encodethe contructor parameters (that may bem.parameterfutures)
If futures did have a map function, you could come up with these kinds of modules:
type Unwrap<T> = T extends Future<infer V> ? V : T;
function map<T extends readonly unknown[] | [], U>(futures: T, f: (values: { -readonly [P in keyof T]: Unwrap<T[P]>; }) => U): Future<U>;
Would allow me to write:
const p: Future<`0x${string}`> = m.map([100n, future(true), 1], ([a, b, c]) => abiEncode(a, b, c))
Which would make the CREATE2 deployment possible (naively).
import Artifact from "MyContract.json";
import { encodeDeployData, encodePacked, getContractAddress } from "viem";
const salt = m.getParameter("salt", ...);
const initCode = m.map(
[Artifact, m.getParameter("arg1", ...), m.getParameter("arg2", ...)],
([{ abi, bytecode }, ...args]) => encodeDeployData({ abi, bytecode, args }),
);
const creationData = m.map(
[salt, initCode],
(args) => encodePacked(["bytes32", "bytes"], args)
)
const transaction = m.send("create-my-contract", CREATE2_DEPLOYER, 0, creationData);
const address = m.map(
[CREATE2_DEPLOYER, salt, initCode],
([from, salt, bytecode]) => getContractAddress({ from, salt, bytecode });
);
const myContract = m.contractAt("MyContract", Artifact, address);
return { transaction, myContract }
I don't know if there are technical limitations with how the execution graph is computed that would make this not possible.
PS: Let me know if this issue should move to the hardhat repo, as it appears that ignition development moved there as well.