Procházet zdrojové kódy

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

Ali Behjati před 1 rokem
rodič
revize
f68b5d5abc

+ 6 - 0
.gitmodules

@@ -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

+ 1 - 1
lazer/contracts/evm/README.md

@@ -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

+ 3 - 0
lazer/contracts/evm/foundry.toml

@@ -2,3 +2,6 @@
 src = "src"
 out = "out"
 libs = ["lib"]
+
+optimizer = true
+optimizer_runs = 100000

+ 1 - 0
lazer/contracts/evm/lib/createx

@@ -0,0 +1 @@
+Subproject commit cbac803268835138f86a69bfe01fcf05a50e0447

+ 1 - 0
lazer/contracts/evm/lib/openzeppelin-contracts

@@ -0,0 +1 @@
+Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79

+ 0 - 20
lazer/contracts/evm/script/PythLazer.s.sol

@@ -1,20 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import {Script, console} from "forge-std/Script.sol";
-import {PythLazer} from "../src/PythLazer.sol";
-
-contract PythLazerScript is Script {
-    PythLazer public pythLazer;
-
-    function setUp() public {}
-
-    function run() public {
-        vm.startBroadcast();
-
-        pythLazer = new PythLazer();
-        pythLazer.initialize(address(1));
-
-        vm.stopBroadcast();
-    }
-}

+ 175 - 0
lazer/contracts/evm/script/PythLazerDeploy.s.sol

@@ -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);
+    }
+}

+ 4 - 2
lazer/contracts/evm/src/PythLazer.sol

@@ -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";
 
@@ -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";
+    }
 }