I am seeing an unexpected behavior when deploying a proxy using defender.deployProxy() with CREATE2: the resulting proxy address on Ethereum Mainnet is different from the address obtained on other networks, even though all parameters and salts are identical.
This does not seem expected, especially when the only changed input relates to another contract whose address should not influence the CREATE2 computation.
Environment
@openzeppelin/hardhat-upgrades: ^3.9.1@openzeppelin/defender-sdk: ^2.5.0- Network: Ethereum Mainnet
- Deployment method:
defender.deployProxy()using CREATE2
Context
We have two deployment steps:
deployImplementation()– used to deploy BContract, which recently had code changes.deployProxy()– used to deploy a factory contract through the following code:
const factory = await this.hre.defender.deployProxy(
factoryFactory,
[MContractAddress, BContractAddress, wallet1Address, wallet2Address],
{
initializer: "initialize",
salt: "0x1234...",
redeployImplementation: "always",
}
);
The deployment of BContract makes sense to differ across networks because its code changed, so the implementation's data_bytes also differs. This part is completely expected.
However, the surprising issue occurs with the proxy deployment:
- The CREATE2 salt is the same across all networks.
- The initializer parameters are the same structure, just with a different
BContractAddress. - The
BContractAddressis only used internally duringinitialize()under a beacon pattern. - It should not affect the CREATE2 address of the deployed proxy itself.
Despite this, the resulting proxy address on Mainnet does not match the expected deterministic address derived from testnets and other environments.
Why this seems incorrect
CREATE2 addresses depend solely on:
- The deployer address
- The salt
- The bytecode + constructor args
In this case:
- The proxy bytecode is identical.
- The initializer parameters are not part of the CREATE2 computation.
- The only parameter that changed (
BContractAddress) should not modify the CREATE2 input data for deploying the proxy. - Other networks consistently produce the same proxy address before the BContract change its code, but Ethereum Mainnet does not.
This suggests that something unexpected happened during the Defender-managed deployment.
What I expected
The proxy deployment address should be identical across all networks as long as:
- The salt is unchanged
- The proxy bytecode is unchanged
- The CREATE2 deployer is the same
Even if BContractAddress differs, that should not impact the CREATE2 proxy address.
Questions
- Is there any scenario where Defender modifies or wraps the deployment bytecode in a way that could cause CREATE2 to output a different address on one network but not others?
- Could the updated
BContractAddresshave had any indirect effect inside Defender's proxy deployment logic? - Is there any additional metadata or intermediate step introduced in Defender that could change the CREATE2 input on one network?
Additional notes
- There were no manual deployments, everything was executed through the Defender API.
- No salt changes were made.
- The same repository, artifacts, and scripts were used.