I felt that the great work 4y ago by @MicahZoltu / @Arachnid with the popular CREATE2 factory “Deterministic Deployment Proxy” needed an update. Many other new blockchains have been appearing and gaining traction, so protection from front-running risk is needed.
In my updated version I’ve hashed caller() with the user-provided salt so that there won’t be an address clash if different accounts deploy a contract with the same user-provided salt:
I’ve also updated dependencies e.g. Solidity version from 0.5.8 to 0.8.21 (the latest), and you can set evmVersion to paris if you need to deploy the factory to any blockchains that don’t yet support PUSH0 opcode at:
The entire point of deterministic deployment proxy is that they can’t be “frontrun”. If you get frontrun it just means someone else paid the gas for you and saved you some money, but the thing they deployed will be exactly the same as the thing you would have deployed.
Any references to msg.sender in a deterministically deployed contract’s constructor are functionally not deterministically deployed.
If you want a specific address to have special rights and you also want a deterministically address, then you should set the address as an constant in the contract so you get the same address on all chains regardless of who triggers the deployment. This will also make it more clear in the code what the intent is, rather than relying on a specific deployment procedure to get the code you want on-chain.
Though msg.sender in a contract’s constructor actually becomes the factory’s address if the contract is deployed via a factory such as the Deterministic Deployment Proxy. A user would realize this when they try it, and then may quickly fix it by replacing msg.sender with tx.origin.
I’ve just tried to deploy (via Nick’s Deterministic Deployment Proxy) an ERC20 contract that grants admin role to the account doing the deployment with this code in the constructor:
_grantRole(DEFAULT_ADMIN_ROLE, tx.origin);
Here’s the output if walletToUse = wallet1:
Using network: hardhat (31337), account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 having 10000.0 of native currency, RPC url: undefined
Address of Arachnid's factory deployed keylessly: 0x4e59b44847b379578588920ca78fbf26c0b4956c
expected address using await walletToUse.call(txData): 0xd8d463e3a19f1ea97e8a62670054515f3f38b740
expected address using ethers.getCreate2Address: 0xd8D463e3a19F1eA97E8A62670054515f3f38B740
Now deploying TESTERC20 using Arachnid's factory...
TESTERC20 was deployed by 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Testing deployed contract by getting contract at address 0xd8D463e3a19F1eA97E8A62670054515f3f38B740:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 calling contract.point(): 10,5
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 calling contract.privilegedFunction(): 1
Here’s the output if walletToUse = wallet2:
Using network: hardhat (31337), account: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 having 10000.0 of native currency, RPC url: undefined
Address of Arachnid's factory deployed keylessly: 0x4e59b44847b379578588920ca78fbf26c0b4956c
expected address using await walletToUse.call(txData): 0xd8d463e3a19f1ea97e8a62670054515f3f38b740
expected address using ethers.getCreate2Address: 0xd8D463e3a19F1eA97E8A62670054515f3f38B740
Now deploying TESTERC20 using Arachnid's factory...
TESTERC20 was deployed by 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Testing deployed contract by getting contract at address 0xd8D463e3a19F1eA97E8A62670054515f3f38B740:
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 calling contract.point(): 10,5
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 calling contract.privilegedFunction(): 1
This shows that 2 different accounts were able to deploy the same contract to the same deterministic address (obtained before deployment using both call and getCreate2Address), and each of them got admin privilege in the contract instance that they deployed. In reality, the owner of wallet2 would have done it on a different blockchain, and the owner of wallet1 may not have wanted wallet2 to do the deployment to that same address and gain control of the contract on that other blockchain.
I’m arguing that what you are doing is an anti-pattern. Ideally, you create immutable contracts with no admin. If an admin is necessary and you want to ensure it is a specific address no matter the blockchain and no matter who deploys it, then what you should be doing is something like this:
This makes it very clear that the specified address is always the initial admin, regardless of who deploys the contract. It is less prone to error, much easier to audit, and doesn’t depend on specialized deployment scripts being executed properly to get the desired result.
The idea here is to keep all of the desired functionality in the contract code, and make it impossible to introduce a security vulnerability due to a problem with the deployment scripts.
Privileged access is often necessary for contracts, depending on use cases. e.g. even a withdraw function to be able to take out tokens that were accidentally sent to the contract requires privileged access, otherwise without a withdraw function the tokens would be permanently stuck which could be very unfortunate for the sender if it was a large amount.
It shouldn’t be possible for different accounts to deploy to the same address, especially for contracts that are designed with privileged access (e.g. admin).
Yes, hard-coding the address when granting admin role in the constructor gives clarity and certainty of who the admin will be, making it imposible for some other account to obtain privileged access.
But as the factory is used by the public, we can’t expect all contracts that use it to have addresses hard-coded like that in their constructors.
So my updated version offers protection for when someone uses the factory to deploy a contract that grants admin role to the deploying account. Hashing the deploying account’s address with the salt ensures that the contract addresses will be different for different accounts that deploy with exactly the same contract bytecode and salt.
Here’s the ouput if account is 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:
Using network: hardhat (31337), account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 having 10000.0 of native currency, RPC url: undefined
Address of factory deployed keylessly: 0xaf45f86eb0bbf0536fa770b699b806f22496d875
expected address using await walletToUse.call(txData): 0xe32dcd55a5daee6e5a34b046f759721c76e78291
expected address using ethers.getCreate2Address: 0xE32dcD55a5daEE6E5A34b046F759721C76E78291
Now deploying TESTERC20 using factory...
TESTERC20 was deployed by 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Testing deployed contract by getting contract at address 0xe32dcd55a5daee6e5a34b046f759721c76e78291:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 calling contract.point(): 10,5
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 calling contract.privilegedFunction(): 1
Notice that the contract address is deployed to 0xE32dcD55a5daEE6E5A34b046F759721C76E78291.
Here’s the ouput if account is 0x70997970C51812dc3A010C7d01b50e0d17dc79C8:
Using network: hardhat (31337), account: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 having 10000.0 of native currency, RPC url: undefined
Address of factory deployed keylessly: 0xaf45f86eb0bbf0536fa770b699b806f22496d875
expected address using await walletToUse.call(txData): 0xbb1070bb427b517cb6ecfc2b1ef08ec413259277
expected address using ethers.getCreate2Address: 0xbb1070bb427B517Cb6Ecfc2B1ef08ec413259277
Now deploying TESTERC20 using factory...
TESTERC20 was deployed by 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Testing deployed contract by getting contract at address 0xbb1070bb427b517cb6ecfc2b1ef08ec413259277:
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 calling contract.point(): 10,5
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 calling contract.privilegedFunction(): 1
Notice that the contract address is deployed to 0xbb1070bb427B517Cb6Ecfc2B1ef08ec413259277, which is different. This shows that with my updated version, a different account now cannot deploy the same contract with same user-provided salt to the same address.
My updated version hashes deploying account address with the user-provided salt, so the address of the contract can be deterministically calculated using ethers like this:
ethers.getCreate2Address(addressOfFactory, ethers.solidityPackedKeccak256([ `address`, `bytes32` ], [ walletToUse.address, salt ]), ethers.keccak256(bytecodeOfContractWithArgs))
These days though I’d suggest using the “CREATE3” method of deploying contracts instead of CREATE2, so that contract code no longer matters to the address.
I explore keyless deployment with the CREATE3 method in detail in this repository:
If your “owner” is a constructor argument then it will become part of the contract’s code and mean that the contract deployed with that specific owner exists at the same address on every blockchain (if using deterministic deployment proxy).
Yes, with CREATE2, creation code affects deployment address. As constructor arguments are part of the creation code, different constructor arguments therefore lead to different deployment addresses. So there is no problem with front-running in that case. But we don’t have control over what code others have in their contracts.
As I’ve described before, a problematic case is when someone uses the deterministic deployment proxy to deploy a contract whose constructor grants privileged access to tx.origin. The original legitimate account that deploys it wouldn’t want other malicious accounts to deploy the same contract to the same address on other blockchains and become admin (by using the same salt, which can be easily found on etherscan etc.). My updated version which factors in the deploying account’s address prevents that.
Though now with the CREATE3 method, it’s even more important to factor in the deploying account’s address, because creation code no longer plays a role in address calculation. Glady, all CREATE3 factories that I’ve seen do so.
The core of our disagreement seems to be on whether this is a ever a good idea. I am of the belief that you should never being doing this. There is no use case I can think of where it is the right choice compared to using a constructor parameter or hard-coded value.
If you really want, you could do require(tx.origin == param1) or something, but even that is a bad idea because anyone who is calling the factory with a contract wallet (e.g., Gnosis SAFE) will not want tx.origin to be the owner, the SAFE should be the owner (which is an intermediate address in the call chain, not tx.origin or msg.sender).
We’re not disagreeing - I already agreed with you that it’s much safer to hardcode the admin’s address in the constructor. I also suggested another option - to pass in the admin address into the constructor. So developers like you and I would do these instead.
But the issue isn’t about developers like you and I, it’s about everyone else. Factories like yours / Nick’s are used by the public to deploy their own contracts. As I had said a couple of times in different ways, we can’t totally stop others from writing code that grants privileged access to msg.sender / tx.origin in their own contracts. OpenZeppelin’s contract wizard even output such code that has msg.sender being granted admin role, minter role etc. in the constructor.
So as experienced technical professionals in this space, we can be on the other side by protecting others where we can from vulnerabilities in their code that they may not even be aware of. Making our products safer is good for the blockchain/web3/DeFi/crypto ecosystem & community. That’s what I’ve done in my update to your / Nick’s popular deterministic deployment proxy.
I really don’t understand the purpose of hashing the caller: if you want the contract’s address to depend on caller + initcode, then simply call CREATE2 …
The whole purpose of the deterministic deployer was to remove the dependency on the caller, and make the code depend on solely on the initcode
And tx.origin should be deprecated by now, and completely avoided. It breaks anything related to contract-based accounts, be it Safe or ERC4337 accounts or anything else.
Hi @dror, it’s nice to see you here (I’m looking forward to OpenGSN 3 full release).
It’s to prevent front-running. Let me explain using variables:
I’ve demonstrated that using Nick’s factory (“Deterministic Deployment Proxy”) (and Zoltu’s which has no salt (it becomes 0)) this happens:
Developer D1 uses the factory to deploy contract C using salt S to address A1 onto blockchain B1.
If developer D2 then uses the factory to deploy contract C using salt S onto blockchain B2, then the contract will also have address A1 on B2.
This would be a problem if C has code in the constructor that grants privileged access to whatever account did the deployment e.g. using tx.origin.
This is “front-running” because D2, who may not be a legitimate actor of the project that has the contract, takes over the address A1 on B2 and possibly taking ownership or control of the contract there, preventing legitimate actor D1 from ever being able to deploy contract C to A1.
By hashing caller with salt, this happens:
Developer D1 uses the factory to deploy contract C using salt S to address A2 onto blockchain B1.
If developer D2 then uses the factory to deploy contract C using salt S onto blockchain B2, then the contract will have address A3 on B2, i.e. an address different from A1.
If D1 deploys contract C using salt S onto B2, it will have address A2 as desired, same as on B1.
So front-running by D2 now can’t occur under any circumstance.
If you check other CREATE2 libraries / factories you’ll also find that they hash the caller with the salt. Here are some examples:
CREATE2 doesn’t use caller to produce an address. It instead uses the address of the contract that calls it (which is the factory). See Yul — Solidity 0.8.22 documentation where it says the address formula is keccak256(0xff . this . s . keccak256(mem[p…(p+n))) where “this is the current contract’s address”.
The purpose was to be able to deploy a contract to the same address on any EVM-based blockchain. CREATE2 made it possible to determine and thus know the address of the contract before it’s deployed, so it was well-suited for that purpose.
So people have been using the factories in order to get the same address for their contracts on multiple blockchains. But they may not be aware of the front-running risk.
As these factories are fully available to use by the public, we have no control over what developers write in the contracts that they deploy using these factories. But we should still try to keep them safe when they use our products.
The purpose of CREATE2 was to create a deterministic address, that only depends on the actual deployed code.
It is possible to abuse it, and deploy arbitrary code into an address. It doesn’t mean that CREATE2 is bad - with proper usage, it works just great.
I’d say that using tx.origin or even msg.sender within a constructor is an abuse of the feature.
I think that if you want the “caller” or “owner” address be part of the target contract address - then you’d better pass it as a constructor parameter.