-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lazer/contracts/evm): add deterministic deployment script (#2137)
- Loading branch information
1 parent
0349da4
commit f68b5d5
Showing
8 changed files
with
191 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,6 @@ | |
src = "src" | ||
out = "out" | ||
libs = ["lib"] | ||
|
||
optimizer = true | ||
optimizer_runs = 100000 |
Submodule openzeppelin-contracts
added at
69c8de
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {Script, console} from "forge-std/Script.sol"; | ||
import {PythLazer} from "../src/PythLazer.sol"; | ||
import {ICreateX} from "createx/ICreateX.sol"; | ||
import {CreateX} from "createx/CreateX.sol"; | ||
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
|
||
// This script deploys the PythLazer proxy and implementation contract using | ||
// CreateX's contract factory to a deterministic address. Having deterministic | ||
// addresses make it easier for users to access our contracts and also helps in | ||
// making this deployment script idempotent without maintaining any state. | ||
// | ||
// CreateX enables us to deploy the contract deterministically to the same | ||
// address on any EVM chain using various methods. We use the deployer address | ||
// in salt to protect the deployment addresses from being redeployed by other | ||
// wallets so the addresses we use be fully deterministic. We use `create2` to | ||
// deploy the implementation contracts (to have a single address per | ||
// implementation) and `create3` to deploy the proxies (to avoid changing | ||
// addresses if our proxy contract changes, which might sound impossible, but | ||
// can easily happen when you change the optimisation or the solc version!). | ||
// Below you will find more explanation on what these methods. | ||
// | ||
// a `create` opcode (which typical deployment uses) on the EVM would deploy a | ||
// contract to an address that is a hash of the caller address, and the tx | ||
// nonce. Nonce is the tx number and can't be set and therefore it's not easily | ||
// possible to achieve deterministic deployments (unless we somehow make sure | ||
// the deployer never sends transactions prior deployment). That's why there is | ||
// a `create2` opcode that allows us to deploy contracts to an address that is | ||
// a hash of the caller address, a salt, and the contract bytecode. The salt is | ||
// a random number that we can control and therefore we can deploy contracts | ||
// deterministically. The problem with `create2` is that it is the bytecode | ||
// affects the address and therefore we can't deploy different contracts to the | ||
// same address. That's where the idea of `create3` comes in. `create3` is a | ||
// wrapper around create2 and create that achieves it. It deploys a minimal | ||
// fixed-code proxy first using create2 (which will be in a deterministic | ||
// address) and then we call the proxy using the new contract code. The proxy | ||
// then calls `create` to deploy the new contract and since it's the first | ||
// transaction of the proxy the nonce will be 0 and the address will be | ||
// deterministic. | ||
// | ||
// Maybe you are wondering why factory contracts are needed. We need them to | ||
// call create2 and since the caller address matters we need a factory contract | ||
// at a fixed address and that's what CreateX is for. CreateX has a single | ||
// signed transaction to deploy it in any network (and hopefully the key that | ||
// it uses is destroyed!). | ||
contract PythLazerDeployScript is Script { | ||
// The address of the wallet calling this script. This is used to protect | ||
// the deployment addresses from being redeployed by other wallets. | ||
address constant deployer = 0x78357316239040e19fC823372cC179ca75e64b81; | ||
|
||
// CreateX is a Contract Factory that provides multiple deployment solutions that | ||
// we use for deterministic deployments of our contract. It is universally deployed | ||
// at this address and can be deployed if it is not already deployed. | ||
ICreateX constant createX = | ||
ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); | ||
|
||
function contractDeployed(address addr) public view returns (bool) { | ||
return addr.code.length > 0; | ||
} | ||
|
||
function setUp() public view { | ||
if (!contractDeployed(address(createX))) { | ||
revert( | ||
"CreateX not deployed. Deploy it following the instructions in https://github.com/pcaversaccio/createx" | ||
); | ||
} | ||
} | ||
|
||
// Generate a salt that is safeguarded against redeployments by other | ||
// deployers. CreateX has a safeguard mechanism that based on the following | ||
// salt format creates a guardedSalt that no one else can get to. | ||
// | ||
// @param seed: A random number to be used as the salt for deployment | ||
function generateSalt( | ||
bytes11 seed | ||
) public pure returns (bytes32 salt, bytes32 guardedSalt) { | ||
salt = bytes32( | ||
abi.encodePacked( | ||
deployer, // Deployment protection by our deployer | ||
uint8(0), // No crosschain replay protection | ||
seed | ||
) | ||
); | ||
|
||
// Mimic CreateX's guardedSalt mechanism only for our type of salt. Couldn't use CreateX's | ||
// _guard function because it is internal and inheriting it results in conflict with Script. | ||
guardedSalt = keccak256( | ||
abi.encode(bytes32(uint256(uint160(deployer))), salt) | ||
); | ||
} | ||
|
||
// Deploy the implementation of the contract. This script is idempotent and | ||
// will return if the implementation is already deployed. | ||
// | ||
// @param seed: A random number to be used as the salt for deployment | ||
function deployImplementation(bytes11 seed) public returns (address addr) { | ||
(bytes32 salt, bytes32 guardedSalt) = generateSalt(seed); | ||
address implAddr = createX.computeCreate2Address({ | ||
salt: guardedSalt, | ||
initCodeHash: keccak256( | ||
abi.encodePacked(type(PythLazer).creationCode) | ||
) | ||
}); | ||
|
||
if (contractDeployed(implAddr)) { | ||
console.log("Implementation already deployed at: %s", implAddr); | ||
return implAddr; | ||
} | ||
|
||
console.log("Deploying implementation... %s", implAddr); | ||
|
||
vm.startBroadcast(); | ||
addr = createX.deployCreate2({ | ||
salt: salt, | ||
initCode: abi.encodePacked(type(PythLazer).creationCode) | ||
}); | ||
vm.stopBroadcast(); | ||
|
||
console.log("Deployed implementation to: %s", addr); | ||
|
||
require( | ||
addr == implAddr, | ||
"Implementation address mismatch due to mismatched deployer." | ||
); | ||
|
||
return addr; | ||
} | ||
|
||
function deployProxy( | ||
bytes11 seed, | ||
address impl | ||
) public returns (address addr) { | ||
(bytes32 salt, bytes32 guardedSalt) = generateSalt(seed); | ||
address proxyAddr = createX.computeCreate3Address({salt: guardedSalt}); | ||
|
||
if (contractDeployed(proxyAddr)) { | ||
console.log("Proxy already deployed at: %s", proxyAddr); | ||
return proxyAddr; | ||
} | ||
|
||
console.log("Deploying proxy... %s", proxyAddr); | ||
|
||
// Set the top authority to the deployer for the time being | ||
address topAuthority = deployer; | ||
|
||
vm.startBroadcast(); | ||
addr = createX.deployCreate3({ | ||
salt: salt, | ||
initCode: abi.encodePacked( | ||
type(ERC1967Proxy).creationCode, | ||
abi.encode( | ||
impl, | ||
abi.encodeWithSignature("initialize(address)", topAuthority) | ||
) | ||
) | ||
}); | ||
vm.stopBroadcast(); | ||
|
||
console.log("Deployed proxy to: %s", addr); | ||
|
||
require( | ||
addr == proxyAddr, | ||
"Proxy address mismatch due to mismatched deployer." | ||
); | ||
|
||
return addr; | ||
} | ||
|
||
function run() public { | ||
address impl = deployImplementation("lazer:impl"); | ||
deployProxy("lazer:proxy", impl); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters