Skip to content

feat(lazer/contracts/evm): add deterministic deployment script #2137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
[submodule "lazer/contracts/evm/lib/openzeppelin-contracts-upgradeable"]
path = lazer/contracts/evm/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "lazer/contracts/evm/lib/openzeppelin-contracts"]
path = lazer/contracts/evm/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lazer/contracts/evm/lib/createx"]
path = lazer/contracts/evm/lib/createx
url = https://github.com/pcaversaccio/createx
2 changes: 1 addition & 1 deletion lazer/contracts/evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ $ anvil
### Deploy

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

### Cast
Expand Down
3 changes: 3 additions & 0 deletions lazer/contracts/evm/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
src = "src"
out = "out"
libs = ["lib"]

optimizer = true
optimizer_runs = 100000
1 change: 1 addition & 0 deletions lazer/contracts/evm/lib/createx
Submodule createx added at cbac80
1 change: 1 addition & 0 deletions lazer/contracts/evm/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at 69c8de
20 changes: 0 additions & 20 deletions lazer/contracts/evm/script/PythLazer.s.sol

This file was deleted.

175 changes: 175 additions & 0 deletions lazer/contracts/evm/script/PythLazerDeploy.s.sol
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);
}
}
6 changes: 4 additions & 2 deletions lazer/contracts/evm/src/PythLazer.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

Expand Down Expand Up @@ -88,9 +87,12 @@ contract PythLazer is OwnableUpgradeable, UUPSUpgradeable {
if (signer == address(0)) {
revert("invalid signature");
}
console.log("recover address %s", signer);
if (!isValidSigner(signer)) {
revert("invalid signer");
}
}

function version() public pure returns (string memory) {
return "0.1.0";
}
}
Loading