feat(`forge script`): offer convenient way of performing idempotent CREATE2 deployments in scripts and tests
I'd like to open a discussion regarding a common challenge with CREATE2 deployments in Foundry scripts and tests.
The core issue is that CREATE2 deployments are not inherently idempotent. Any attempt to deploy to an address that already contains code will cause the transaction to revert, halting the entire script or test.
This creates significant friction in two key workflows:
-
Deployment Scripts (
forge script): When running a deployment for a multi-contract system, the script fails if it encounters a contract that hasn't changed from a deployed version and already exists on-chain. This makes robust, repeatable deployments difficult without adding cumbersome manual checks. -
Fork Testing (
forge test): When writing fork tests, thesetUpfunction will fail if the contracts it needs to deploy for the test environment already exist on the forked chain. This limits our ability to write tests against many real-world network states.
This leads me to the main questions for this discussion:
- What are the current recommended best practices within the Foundry ecosystem for handling this? Is there an idiomatic, clean way to achieve idempotent deployments that I might be missing?
- If a clean, built-in pattern doesn't currently exist, would the Foundry team be open to exploring infrastructure to make this workflow more robust and ergonomic?
I believe addressing this would be a great quality-of-life improvement for developers building complex, deterministic systems.
Hi @zzh1996 thanks for your suggestion!
I like the idea of making forge script behave much more like a database migration than a one-off bash script. This also ties into our goal of it being easy for users to actually test their deployment scripts (and include it in the scope of their audits).
We currently do not offer a convenient way of doing this, users rely on creating a deployment infrastructure themselves, for a reference example see: https://github.com/euler-xyz/evk-periphery/tree/development/script.
We would be open to exploring / improving forge script to accommodate this use case better.
@legion2002 would this also be useful to you?
This also ties into --verify, we need to make sure that if the contracts are already deployed we do attempt verifying
It would be awesome if Forge supported idempotent CREATE2 deployments.
Maybe this behavior could toggled through a new set of Vm functions:
// only impacts the next creation, similar to `prank`
vm.ignoreCreate2Collision();
// starts impacting all creations, similar to `startPrank`
vm.startIgnoringCreate2Collisions();
// stops impacting all creations, similar to `stopPrank`
vm.stopIgnoringCreate2Collisions();
When such a feature were turned on, Forge would, in the case of a top-level CREATE2 collision:
- Demote the creation collision from an error to a warning (or even silence it)
- Make the contract creation expression evaluate to the already-deployed contract
I like the idea of adding cheatcodes for tests. But I don't think we should do this for scripts, because I never want my scripts to silently fail. If I really want to ignore the create2 error, we can just check the predicted create2 address for code, before sending the call to the factory.
If I really want to ignore the create2 error, we can just check the predicted create2 address for code, before sending the call to the factory.
You're right. It is possible to compute the address of a contract deployed via CREATE2, but if you are doing the exact same check for all the contracts in your contract suite, it can become repetitive:
contract MyContract {
constructor(uint256) {}
}
function deployContract(uint256 arg) external returns (MyContract myContract) {
bytes32 salt;
bytes memory creationCode = type(MyContract).creationCode;
bytes memory initCode = abi.encodePacked(creationCode, arg);
bytes32 initCodeHash = keccak256(initCode);
address expectedAddress = vm.computeCreate2Address(salt, initCodeHash);
if (expectedAddress.code.length == 0) {
vm.startBroadcast();
myContract = new MyContract{salt: salt}(arg);
vm.stopBroadcast();
vm.assertEq(address(myContract), expectedAddress);
} else {
myContract = MyContract(expectedAddress);
}
}
Classic Soldiity currently does not support generics and is unlikely to support it any time soon (according to their article "The Road to Core Solidity"). If this feature is ever supported, it will be on the upcoming Core Solidity. When that time comes, maybe we'll have an idiomatic (and less repetitive) way to replicate this pattern for a large suite of contracts. However, we don't know when Core Solidity will be available and stable...