Skip to content

Commit f68b5d5

Browse files
authored
feat(lazer/contracts/evm): add deterministic deployment script (#2137)
1 parent 0349da4 commit f68b5d5

File tree

8 files changed

+191
-23
lines changed

8 files changed

+191
-23
lines changed

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@
44
[submodule "lazer/contracts/evm/lib/openzeppelin-contracts-upgradeable"]
55
path = lazer/contracts/evm/lib/openzeppelin-contracts-upgradeable
66
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
7+
[submodule "lazer/contracts/evm/lib/openzeppelin-contracts"]
8+
path = lazer/contracts/evm/lib/openzeppelin-contracts
9+
url = https://github.com/OpenZeppelin/openzeppelin-contracts
10+
[submodule "lazer/contracts/evm/lib/createx"]
11+
path = lazer/contracts/evm/lib/createx
12+
url = https://github.com/pcaversaccio/createx

lazer/contracts/evm/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ $ anvil
3939
### Deploy
4040

4141
```shell
42-
$ forge script script/PythLazer.s.sol:PythLazerScript --rpc-url <your_rpc_url> --private-key <your_private_key>
42+
$ forge script script/PythLazerDeploy.s.sol --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcast
4343
```
4444

4545
### Cast

lazer/contracts/evm/foundry.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
6+
optimizer = true
7+
optimizer_runs = 100000

lazer/contracts/evm/lib/createx

Submodule createx added at cbac803

lazer/contracts/evm/script/PythLazer.s.sol

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {Script, console} from "forge-std/Script.sol";
5+
import {PythLazer} from "../src/PythLazer.sol";
6+
import {ICreateX} from "createx/ICreateX.sol";
7+
import {CreateX} from "createx/CreateX.sol";
8+
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
9+
10+
// This script deploys the PythLazer proxy and implementation contract using
11+
// CreateX's contract factory to a deterministic address. Having deterministic
12+
// addresses make it easier for users to access our contracts and also helps in
13+
// making this deployment script idempotent without maintaining any state.
14+
//
15+
// CreateX enables us to deploy the contract deterministically to the same
16+
// address on any EVM chain using various methods. We use the deployer address
17+
// in salt to protect the deployment addresses from being redeployed by other
18+
// wallets so the addresses we use be fully deterministic. We use `create2` to
19+
// deploy the implementation contracts (to have a single address per
20+
// implementation) and `create3` to deploy the proxies (to avoid changing
21+
// addresses if our proxy contract changes, which might sound impossible, but
22+
// can easily happen when you change the optimisation or the solc version!).
23+
// Below you will find more explanation on what these methods.
24+
//
25+
// a `create` opcode (which typical deployment uses) on the EVM would deploy a
26+
// contract to an address that is a hash of the caller address, and the tx
27+
// nonce. Nonce is the tx number and can't be set and therefore it's not easily
28+
// possible to achieve deterministic deployments (unless we somehow make sure
29+
// the deployer never sends transactions prior deployment). That's why there is
30+
// a `create2` opcode that allows us to deploy contracts to an address that is
31+
// a hash of the caller address, a salt, and the contract bytecode. The salt is
32+
// a random number that we can control and therefore we can deploy contracts
33+
// deterministically. The problem with `create2` is that it is the bytecode
34+
// affects the address and therefore we can't deploy different contracts to the
35+
// same address. That's where the idea of `create3` comes in. `create3` is a
36+
// wrapper around create2 and create that achieves it. It deploys a minimal
37+
// fixed-code proxy first using create2 (which will be in a deterministic
38+
// address) and then we call the proxy using the new contract code. The proxy
39+
// then calls `create` to deploy the new contract and since it's the first
40+
// transaction of the proxy the nonce will be 0 and the address will be
41+
// deterministic.
42+
//
43+
// Maybe you are wondering why factory contracts are needed. We need them to
44+
// call create2 and since the caller address matters we need a factory contract
45+
// at a fixed address and that's what CreateX is for. CreateX has a single
46+
// signed transaction to deploy it in any network (and hopefully the key that
47+
// it uses is destroyed!).
48+
contract PythLazerDeployScript is Script {
49+
// The address of the wallet calling this script. This is used to protect
50+
// the deployment addresses from being redeployed by other wallets.
51+
address constant deployer = 0x78357316239040e19fC823372cC179ca75e64b81;
52+
53+
// CreateX is a Contract Factory that provides multiple deployment solutions that
54+
// we use for deterministic deployments of our contract. It is universally deployed
55+
// at this address and can be deployed if it is not already deployed.
56+
ICreateX constant createX =
57+
ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed);
58+
59+
function contractDeployed(address addr) public view returns (bool) {
60+
return addr.code.length > 0;
61+
}
62+
63+
function setUp() public view {
64+
if (!contractDeployed(address(createX))) {
65+
revert(
66+
"CreateX not deployed. Deploy it following the instructions in https://github.com/pcaversaccio/createx"
67+
);
68+
}
69+
}
70+
71+
// Generate a salt that is safeguarded against redeployments by other
72+
// deployers. CreateX has a safeguard mechanism that based on the following
73+
// salt format creates a guardedSalt that no one else can get to.
74+
//
75+
// @param seed: A random number to be used as the salt for deployment
76+
function generateSalt(
77+
bytes11 seed
78+
) public pure returns (bytes32 salt, bytes32 guardedSalt) {
79+
salt = bytes32(
80+
abi.encodePacked(
81+
deployer, // Deployment protection by our deployer
82+
uint8(0), // No crosschain replay protection
83+
seed
84+
)
85+
);
86+
87+
// Mimic CreateX's guardedSalt mechanism only for our type of salt. Couldn't use CreateX's
88+
// _guard function because it is internal and inheriting it results in conflict with Script.
89+
guardedSalt = keccak256(
90+
abi.encode(bytes32(uint256(uint160(deployer))), salt)
91+
);
92+
}
93+
94+
// Deploy the implementation of the contract. This script is idempotent and
95+
// will return if the implementation is already deployed.
96+
//
97+
// @param seed: A random number to be used as the salt for deployment
98+
function deployImplementation(bytes11 seed) public returns (address addr) {
99+
(bytes32 salt, bytes32 guardedSalt) = generateSalt(seed);
100+
address implAddr = createX.computeCreate2Address({
101+
salt: guardedSalt,
102+
initCodeHash: keccak256(
103+
abi.encodePacked(type(PythLazer).creationCode)
104+
)
105+
});
106+
107+
if (contractDeployed(implAddr)) {
108+
console.log("Implementation already deployed at: %s", implAddr);
109+
return implAddr;
110+
}
111+
112+
console.log("Deploying implementation... %s", implAddr);
113+
114+
vm.startBroadcast();
115+
addr = createX.deployCreate2({
116+
salt: salt,
117+
initCode: abi.encodePacked(type(PythLazer).creationCode)
118+
});
119+
vm.stopBroadcast();
120+
121+
console.log("Deployed implementation to: %s", addr);
122+
123+
require(
124+
addr == implAddr,
125+
"Implementation address mismatch due to mismatched deployer."
126+
);
127+
128+
return addr;
129+
}
130+
131+
function deployProxy(
132+
bytes11 seed,
133+
address impl
134+
) public returns (address addr) {
135+
(bytes32 salt, bytes32 guardedSalt) = generateSalt(seed);
136+
address proxyAddr = createX.computeCreate3Address({salt: guardedSalt});
137+
138+
if (contractDeployed(proxyAddr)) {
139+
console.log("Proxy already deployed at: %s", proxyAddr);
140+
return proxyAddr;
141+
}
142+
143+
console.log("Deploying proxy... %s", proxyAddr);
144+
145+
// Set the top authority to the deployer for the time being
146+
address topAuthority = deployer;
147+
148+
vm.startBroadcast();
149+
addr = createX.deployCreate3({
150+
salt: salt,
151+
initCode: abi.encodePacked(
152+
type(ERC1967Proxy).creationCode,
153+
abi.encode(
154+
impl,
155+
abi.encodeWithSignature("initialize(address)", topAuthority)
156+
)
157+
)
158+
});
159+
vm.stopBroadcast();
160+
161+
console.log("Deployed proxy to: %s", addr);
162+
163+
require(
164+
addr == proxyAddr,
165+
"Proxy address mismatch due to mismatched deployer."
166+
);
167+
168+
return addr;
169+
}
170+
171+
function run() public {
172+
address impl = deployImplementation("lazer:impl");
173+
deployProxy("lazer:proxy", impl);
174+
}
175+
}

lazer/contracts/evm/src/PythLazer.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma solidity ^0.8.13;
33

4-
import {console} from "forge-std/console.sol";
54
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
65
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
76

@@ -88,9 +87,12 @@ contract PythLazer is OwnableUpgradeable, UUPSUpgradeable {
8887
if (signer == address(0)) {
8988
revert("invalid signature");
9089
}
91-
console.log("recover address %s", signer);
9290
if (!isValidSigner(signer)) {
9391
revert("invalid signer");
9492
}
9593
}
94+
95+
function version() public pure returns (string memory) {
96+
return "0.1.0";
97+
}
9698
}

0 commit comments

Comments
 (0)