├── .gitignore ├── .gitmodules ├── .prettierrc ├── .vscode ├── settings.json └── tasks.json ├── README.md ├── example ├── foundry.toml ├── remappings.txt ├── script │ ├── deploy.s.sol │ └── mint.s.sol ├── src │ ├── ExampleNFT.sol │ └── ExampleSetupScript.sol └── test │ └── ExampleNFT.t.sol ├── exampleOZ ├── foundry.toml ├── remappings.txt ├── script │ ├── deploy.s.sol │ └── mint.s.sol ├── src │ ├── ExampleNFT.sol │ └── ExampleSetupScript.sol └── test │ └── ExampleNFT.t.sol ├── foundry.toml ├── remappings.txt └── src └── UpgradeScripts.sol /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | broadcast 8 | 9 | json 10 | 11 | #Hardhat files 12 | cache 13 | artifacts 14 | 15 | out 16 | **/deployments 17 | 18 | !.vscode 19 | !.prettierrc 20 | !.gas-snapshot 21 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "exampleOZ/lib/openzeppelin-contracts-upgradeable"] 5 | path = exampleOZ/lib/openzeppelin-contracts-upgradeable 6 | url = https://github.com/Openzeppelin/openzeppelin-contracts-upgradeable 7 | [submodule "exampleOZ/lib/openzeppelin-contracts"] 8 | path = exampleOZ/lib/openzeppelin-contracts 9 | url = https://github.com/Openzeppelin/openzeppelin-contracts 10 | [submodule "exampleOZ/lib/forge-std"] 11 | path = exampleOZ/lib/forge-std 12 | url = https://github.com/foundry-rs/forge-std 13 | [submodule "example/lib/forge-std"] 14 | path = example/lib/forge-std 15 | url = https://github.com/foundry-rs/forge-std 16 | [submodule "exampleOZ/lib/UDS"] 17 | path = exampleOZ/lib/UDS 18 | url = https://github.com/0xPhaze/UDS 19 | [submodule "example/lib/UDS"] 20 | path = example/lib/UDS 21 | url = https://github.com/0xPhaze/UDS 22 | [submodule "lib/UDS"] 23 | path = lib/UDS 24 | url = https://github.com/0xPhaze/UDS 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.15", 3 | "files.associations": { 4 | ".gas-snapshot": "julia" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "command": "forge test -vvv", 7 | "group": { 8 | "kind": "test", 9 | "isDefault": true 10 | }, 11 | "label": "forge test", 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upgrade Scripts (WIP) 2 | 3 | Scripts to automate keeping track of active deployments and upgrades. Allows for: 4 | - automatic contract deployments and proxy upgrades if the source has changed 5 | - keeping track of all latest deployments and having one set-up for unit-tests, deployments and interactions 6 | - storage layout compatibility checks on upgrades 7 | 8 | These scripts use [ERC1967Proxy](https://github.com/0xPhaze/UDS/blob/master/src/proxy/ERC1967Proxy.sol) (the relevant functions can be overridden, see [deploying custom proxies](#deploying-custom-proxies)). 9 | 10 | ## Example SetUp Script 11 | 12 | This example is from [ExampleSetupScript](./example/src/ExampleSetupScript.sol). 13 | 14 | ```solidity 15 | contract ExampleSetupScript is UpgradeScripts { 16 | ExampleNFT nft; 17 | 18 | function setUpContracts() internal { 19 | address implementation = setUpContract("ExampleNFT"); 20 | 21 | bytes memory initCall = abi.encodeCall(ExampleNFT.init, ("My NFT", "NFTX")); 22 | address proxy = setUpProxy(implementation, initCall); 23 | 24 | nft = ExampleNFT(proxy); 25 | } 26 | } 27 | ``` 28 | 29 | Running this script on a live network will deploy the *implementation contract* and the *proxy contract* **once**. 30 | Re-running this script without the implementation having changed **won't do anything**. 31 | Re-running this script with a new implementation will detect the change and deploy a new implementation contract. 32 | It will perform a **storage layout compatibility check** and **update your existing proxy** to point to it. 33 | All *current* deployments are updated in `deployments/{chainid}/deploy-latest.json`. 34 | 35 | 36 | ## SetUpContract / SetUpProxy 37 | 38 | This will make sure that `MyContract` is deployed and kept up-to-date. 39 | If the `.creationCode` of `MyContract` ever changes, it will re-deploy the contract. 40 | The hash of `.creationCode` is compared instead of `addr.codehash`, because 41 | this would not allow for reliable checks for contracts that use immutable variables that change for each implementation (such as using `address(this)` in EIP-2612's `DOMAIN_SEPARATOR`). 42 | 43 | ```solidity 44 | string memory contractName = "MyContract"; // name of the contract to be deployed 45 | bytes memory constructorArgs = abi.encode(arg1, arg2); // abi-encoded args (optional) 46 | string memory key = "MyContractImplementation"; // identifier/key to be used for json (optional, defaults to `contractName`) 47 | bool attachOnly = false; // don't deploy, only read from latest-deployment and "attach" (optional, defaults to `false`) 48 | 49 | address implementation = setUpContract(contractName, constructorArgs, key, attachOnly); 50 | ``` 51 | 52 | The `key` is used for display in the console and as an identifier in `deployments/{chainid}/deploy-latest.json`. 53 | Setting up multiple contracts/proxies of the same type requires different keys to be set. 54 | 55 | 56 | Similarly, a proxy can be deployed and kept up-to-date via `setUpProxy`. 57 | 58 | ```solidity 59 | bytes memory initCall = abi.encodeCall(MyContract.init, ()); // data to pass to proxy for making an initial call during deployment (optional) 60 | string memory key = "MyContractProxy"; // identifier/key to be used for json (optional, defaults to `${contractNameImplementation}Proxy`) 61 | bool attachOnly = false; // (optional, defaults to `false`) 62 | 63 | address proxy = setUpProxy(implementationAddress, initCall, key, attachOnly); 64 | ``` 65 | 66 | Storage layout mappings are stored for each proxy implementation. 67 | These are used for *storage layout compatibility* checks when running upgrades. 68 | This requires the implementation contract to be set up using `setUpContract` 69 | for the script to know what storage layout to store for the proxy. 70 | It is best to run through a complete example to understand when/how this is done. 71 | 72 | 73 | ## Example Tutorial using Anvil 74 | 75 | First, make sure [Foundry](https://book.getfoundry.sh) is installed. 76 | 77 | 1. Clone the repository: 78 | ```sh 79 | git clone https://github.com/0xPhaze/upgrade-scripts 80 | ``` 81 | 82 | 2. Navigate to the example directory and install the dependencies 83 | ```sh 84 | cd upgrade-scripts/example 85 | forge install 86 | ``` 87 | 88 | 3. Spin up a local anvil node **in a second terminal**. 89 | ```sh 90 | anvil 91 | ``` 92 | 93 | Read through [deploy.s.sol](./example/script/deploy.s.sol) before running random scripts from the internet using `--ffi`. 94 | 95 | 4. In the example project root, run 96 | ```sh 97 | UPGRADE_SCRIPTS_DRY_RUN=true forge script deploy --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --ffi 98 | ``` 99 | to go through a "dry-run" of the deploy scripts. 100 | This connects to your running anvil node using the default account's private key. 101 | 102 | 5. Add `--broadcast` to the command to actually broadcast the transactions on-chain. 103 | ```sh 104 | forge script deploy --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --broadcast --ffi 105 | ``` 106 | 107 | After a successful run, it should have created the file `./example/deployments/31337/deploy-latest.json` which keeps track of your up-to-date deployments. It also saves the contracts *creation code hash* and its *storage layout*. 108 | 109 | 6. Try running the command again. 110 | It will detect that no implementation has changed and thus not create any new transactions. 111 | 112 | ## Upgrading a Proxy Implementation 113 | 114 | If any registered contracts' implementation changes, this should be detected and the corresponding proxies should automatically get updated on another call. 115 | Try changing the implementation by, for example, uncommenting the line in `tokenURI()` in [ExampleNFT.sol](./example/src/ExampleNFT.sol) and re-running the script. 116 | 117 | ```solidity 118 | contract ExampleNFT { 119 | ... 120 | function tokenURI(uint256 id) public view override returns (string memory uri) { 121 | // uri = "abcd"; 122 | } 123 | } 124 | ``` 125 | 126 | After a successful upgrade, running the script once more will not broadcast any additional transactions. 127 | 128 | ## Detecting Storage Layout Changes 129 | 130 | A main security-feature of these scripts is to detect storage-layout changes. 131 | Try uncommenting the following line in [ExampleNFT.sol](./example/src/ExampleNFT.sol). 132 | 133 | ```solidity 134 | contract ExampleNFT is UUPSUpgrade, ERC721UDS, OwnableUDS { 135 | // uint256 public contractId = 1; 136 | ... 137 | } 138 | ``` 139 | 140 | This adds an extra variable `contractId` to the storage of `ExampleNFT`. 141 | If the script is run again (note that `--ffi` needs to be enabled), 142 | it should notify that a storage layout change has been detected: 143 | ```diff 144 | Storage layout compatibility check [0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 <-> 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9]: fail 145 | 146 | Diff: 147 | [...] 148 | 149 | 150 | If you believe the storage layout is compatible, add the following to the beginning of `run()` in your deploy script. 151 | ` 152 | isUpgradeSafe[31337][0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0][0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9] = true; 153 | ` 154 | ``` 155 | 156 | Note that, this can easily lead to false-positives, for example, when any variable is renamed 157 | or when, like in this case, a variable is appended correctly to the end of existing storage. 158 | Thus any positive detection here requires manually review. 159 | 160 | Another peculiarity to account for is that, since dry-run uses `vm.prank` instead of `vm.broadcast`, there might be some differences when calculating the addresses of newly deployed contracts. Thus, sometimes, the scripts need to be run without a dry-run to get the correct address to be marked as "upgrade-safe". 161 | 162 | Since we know it is safe, we can add the line 163 | ```solidity 164 | isUpgradeSafe[31337][0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0][0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9] = true; 165 | ``` 166 | to the start of `run()` in [deploy.s.sol](./example/script/deploy.s.sol). 167 | If we re-run the script now, it will deploy a new implementation, perform the upgrade for our proxy and update the contract addresses in `deploy-latest.json`. 168 | 169 | ## Extra Notes 170 | 171 | ### Environment Variables 172 | 173 | These variables can be set in before running a script, by overriding `setUpUpgradeScripts()` 174 | or by passing them in with the command line. They can also be abbreviated (`US_RESET=true forge script ...`). 175 | 176 | ```solidity 177 | bool UPGRADE_SCRIPTS_RESET; // re-deploys all contracts 178 | bool UPGRADE_SCRIPTS_BYPASS; // deploys contracts without any checks whatsoever 179 | bool UPGRADE_SCRIPTS_DRY_RUN; // doesn't overwrite new deployments in deploy-latest.json 180 | bool UPGRADE_SCRIPTS_ATTACH_ONLY; // doesn't deploy contracts, just attaches with checks 181 | bool UPGRADE_SCRIPTS_BYPASS_SAFETY; // bypass all upgrade safety checks 182 | ``` 183 | 184 | ### Accessing Deployments from other Chains 185 | 186 | Deployed addresses from other chains can be accessed via `loadLatestDeployedAddress(key, chainId)`: 187 | ```solidity 188 | address latestFxRootTunnel = loadLatestDeployedAddress("RootTunnelProxy", rootChainId); // will be address(0) if not found 189 | ``` 190 | 191 | ### Additional init Scripts 192 | 193 | ```solidity 194 | if (isFirstTimeDeployed(addr)) { 195 | // ... do stuff when the proxy is deployed for the first time 196 | } 197 | ``` 198 | 199 | ### Deploying Custom Proxies 200 | 201 | All functions in *UpgradeScripts* can be overridden. 202 | These functions in particular might be of interest to override. 203 | 204 | ```solidity 205 | function getDeployProxyCode(address implementation, bytes memory initCall) internal virtual returns (bytes memory) { 206 | // ... 207 | } 208 | 209 | function upgradeProxy(address proxy, address newImplementation) internal virtual { 210 | // ... 211 | } 212 | 213 | function deployCode(bytes memory code) internal virtual returns (address addr) { 214 | // ... 215 | } 216 | ``` 217 | 218 | See [exampleOZ/ExampleSetupScript.sol](./exampleOZ/src/ExampleSetupScript.sol) for a 219 | complete example using OpenZeppelin's upgradeable contracts. 220 | 221 | 222 | ### Running on Mainnet 223 | If not running on a testnet, adding a confirmation through the current timestamp will be necessary, i.e. adding `mainnetConfirmation = 1667499028;`. This is an additional safety measure. 224 | 225 | ### Testing with Upgrade Scripts 226 | 227 | In order to keep the deployment as close to the testing environment, 228 | it is generally helpful to share the same contract set-up scripts. 229 | 230 | To disable any additional checks or logs that are not necessary when running `forge test`, 231 | the function `setUpUpgradeScripts()` can be overridden to 232 | include `UPGRADE_SCRIPTS_BYPASS = true;`. This can be seen in [ExampleNFT.t.sol](./example/test/ExampleNFT.t.sol). 233 | This bypasses all checks and simply deploys the contracts. 234 | 235 | ### Interacting with Deployed Contracts 236 | 237 | To be able to interact with deployed contracts, the existing contracts can 238 | be "attached" to the current environment (instead of re-deploying). 239 | An example of how this can be done in order to mint an NFT from a deployed 240 | address is shown in [mint.s.sol](./example/script/mint.s.sol). 241 | This requires the previous steps to be completed. 242 | 243 | The script can then be run via: 244 | ```sh 245 | forge script mint --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --broadcast 246 | ``` 247 | 248 | ### What is '/data/'? Should this be committed? 249 | 250 | The files in '/data/' are there 1) to tell whether a deployment's contract code has changed and needs to be re-deployed and 2) to determine whether an upgrade is safe. 251 | 1) '*.creation-code-hash' stores the hash of the complete creation code which is used for detecting any code changes. If the the scripts can't find the relevant '.creation-code-hash' file, it will just assume that a new deployment is necessary. 252 | 2) '*.storage-layout' keeps track of the storage layout files tied to the specific deployment addresses. This is used to ensure that the storage layout's between the old and the new implementation contracts are compatible. If the relevant '.storage-layout' file is not found for an address, the script will complain. This means the user needs to manually approve the upgrade. 253 | 254 | 255 | ### Contract Storage Layout Incompatible Example 256 | 257 | Here is an example of what a incompatible contract storage layout change could look like: 258 | 259 | ```diff 260 | "label": "districts", | "label": "sharesRegistered", 261 | "type": "t_mapping(t_uint256,t_struct(District)40351_storage)" | "type": "t_mapping(t_uint256,t_bool)" 262 | "astId": 40369, | "astId": 40531, 263 | "label": "gangsters", | "label": "districts", 264 | "type": "t_mapping(t_uint256,t_struct(Gangster)40314_storage)" | "type": "t_mapping(t_uint256,t_struct(District)40514_storage)" 265 | "astId": 40373, | "astId": 40536, 266 | "label": "itemCost", | "label": "gangsters", 267 | > "type": "t_mapping(t_uint256,t_struct(Gangster)40477_storage)" 268 | > }, 269 | > { 270 | > "astId": 40540, 271 | > "contract": "src/GangWar.sol:GangWar", 272 | > "label": "itemCost", 273 | > "offset": 0, 274 | > "slot": "7", 275 | "astId": 40377, | "astId": 40544, 276 | "slot": "7", | "slot": "8", 277 | ``` 278 | 279 | Here, an additional `mapping(uint256 => bool) sharesRegistered` (right side) was inserted in a storage slot 280 | where previously another mapping existed, shifting the slots of the other variables. 281 | The variable `itemCost`, previously `slot 7` (left side) is now located at `slot 8`. 282 | Running an upgrade with this change would lead to storage layout conflicts. 283 | 284 | Using some diff-tool viewer (such as vs-code's right-click > compare selected) can often paint a clearer picture. 285 | ![image](https://user-images.githubusercontent.com/103113487/186721360-6dee87fe-ad9a-431e-8d0a-2ad9ce601406.png) 286 | 287 | ## Notes and disclaimers 288 | These scripts do not replace manual review and caution must be taken when upgrading contracts 289 | in any case. 290 | Make sure you understand what the scripts are doing. I am not responsible for any damages created. 291 | 292 | Note that, it currently is not possible to detect whether `--broadcast` is enabled. 293 | Thus the script can't reliably detect whether the transactions are only simulated or sent 294 | on-chain. For that reason, when `--broadcast` is not set, `UPGRADE_SCRIPT_DRY_RUN=true` must ALWAYS passed in. 295 | Otherwise this will update `deploy-latest.json` with addresses that haven't actually been deployed yet and will complain on the next run. 296 | 297 | When `deploy-latest.json` was updated with incorrect addresses for this reason, just delete the file and the incorrect previously created `deploy-{latestTimestamp}.json` (containing the highest latest timestamp) and copy the correct `.json` (second highest timestamp) to `deploy-latest.json`. 298 | 299 | If anvil is restarted, these deployments will also be invalid. 300 | Simply delete the corresponding folder `rm -rf deployments/31337` in this case. 301 | -------------------------------------------------------------------------------- /example/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | bytecode_hash = "none" 3 | fs_permissions = [{ access = "read-write", path = "./" }] 4 | -------------------------------------------------------------------------------- /example/remappings.txt: -------------------------------------------------------------------------------- 1 | UDS/=lib/UDS/src/ 2 | upgrade-scripts/=../src/ 3 | forge-std/=lib/forge-std/src/ 4 | -------------------------------------------------------------------------------- /example/script/deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../src/ExampleSetupScript.sol"; 5 | 6 | /* 7 | 1. Start anvil: 8 | ``` 9 | anvil 10 | ``` 11 | 12 | 2. Simulate dry-run on anvil: 13 | ``` 14 | US_DRY_RUN=true forge script deploy --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --ffi 15 | ``` 16 | 17 | 3. Broadcast transactions to anvil: 18 | ``` 19 | forge script deploy --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvv --broadcast --ffi 20 | ``` 21 | 22 | Running the above script again will have no effect as long as state on anvil persists. 23 | If anvil is reset, you'll have to run the above commands with `US_RESET=true` in order to ignore the file containing invalid deployments. 24 | 25 | **/ 26 | 27 | contract deploy is ExampleSetupScript { 28 | function run() external { 29 | // uncommenting this line would mark the two contracts as having a compatible storage layout 30 | // isUpgradeSafe[31337][0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0][0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9] = true; // prettier-ignore 31 | 32 | // uncomment with current timestamp to confirm deployments on mainnet for 15 minutes or always allow via (block.timestamp) 33 | // mainnetConfirmation = 1667499028; 34 | 35 | // will run `vm.startBroadcast();` if ffi is enabled 36 | // ffi is required for running storage layout compatibility checks 37 | // if ffi is disabled, it will enter "dry-run" and 38 | // run `vm.startPrank(tx.origin)` instead for the script to be consistent 39 | upgradeScriptsBroadcast(); 40 | 41 | // run the setup scripts 42 | setUpContracts(); 43 | 44 | // we don't need broadcast from here on 45 | tryStopBroadcast(); 46 | 47 | // run an "integration test" 48 | integrationTest(); 49 | 50 | // console.log and store these in `deployments/{chainid}/deploy-latest.json` (if not in dry-run) 51 | storeLatestDeployments(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/script/mint.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../src/ExampleSetupScript.sol"; 5 | 6 | /* 7 | # Anvil Dry-Run (make sure it is running): 8 | US_DRY_RUN=true forge script mint --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --ffi 9 | 10 | # Broadcast: 11 | forge script mint --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvv --broadcast --ffi*/ 12 | 13 | contract mint is ExampleSetupScript { 14 | function setUpUpgradeScripts() internal override { 15 | // We only want to attach existing contracts. 16 | // Though if everything is up-to-date, this should be redundant and not needed. 17 | UPGRADE_SCRIPTS_ATTACH_ONLY = true; // disables re-deploying/upgrading 18 | 19 | // The following variables can all be set to change the behavior of the scripts. 20 | // These can also all be set through passing the argument in the command line 21 | // e.g: UPGRADE_SCRIPTS_RESET=true forge script ... 22 | 23 | // bool UPGRADE_SCRIPTS_RESET; // re-deploys all contracts 24 | // bool UPGRADE_SCRIPTS_BYPASS; // deploys contracts without any checks whatsoever 25 | // bool UPGRADE_SCRIPTS_DRY_RUN; // doesn't overwrite new deployments in deploy-latest.json 26 | // bool UPGRADE_SCRIPTS_ATTACH_ONLY; // doesn't deploy contracts, just attaches with checks 27 | } 28 | 29 | function run() external { 30 | // run the setup scripts; attach contracts 31 | setUpContracts(); 32 | 33 | upgradeScriptsBroadcast(); 34 | 35 | // do stuff 36 | nft.mint(msg.sender); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/src/ExampleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC721UDS} from "UDS/tokens/ERC721UDS.sol"; 5 | import {OwnableUDS} from "UDS/auth/OwnableUDS.sol"; 6 | import {UUPSUpgrade} from "UDS/proxy/UUPSUpgrade.sol"; 7 | 8 | contract ExampleNFT is UUPSUpgrade, ERC721UDS, OwnableUDS { 9 | uint256 totalSupply; 10 | uint256 public immutable version; 11 | 12 | // uint256 public contractId = 1; 13 | 14 | constructor(uint256 version_) { 15 | version = version_; 16 | } 17 | 18 | function init(string memory name, string memory symbol) external initializer { 19 | __Ownable_init(); 20 | __ERC721_init(name, symbol); 21 | } 22 | 23 | function tokenURI(uint256) public pure override returns (string memory uri) { 24 | // uri = "abcd"; 25 | } 26 | 27 | function mint(address to) public { 28 | _mint(to, ++totalSupply); 29 | } 30 | 31 | function _authorizeUpgrade(address) internal override onlyOwner {} 32 | } 33 | -------------------------------------------------------------------------------- /example/src/ExampleSetupScript.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ExampleNFT} from "./ExampleNFT.sol"; 5 | import {UpgradeScripts} from "upgrade-scripts/UpgradeScripts.sol"; 6 | 7 | contract ExampleSetupScript is UpgradeScripts { 8 | ExampleNFT nft; 9 | 10 | function setUpContracts() internal { 11 | // encodes constructor call: `ExampleNFT(1)` 12 | bytes memory constructorArgs = abi.encode(uint256(1)); 13 | address implementation = setUpContract("ExampleNFT", constructorArgs); 14 | 15 | // encodes function call: `ExampleNFT.init("My NFT", "NFTX")` 16 | bytes memory initCall = abi.encodeCall(ExampleNFT.init, ("My NFT", "NFTX")); 17 | address proxy = setUpProxy(implementation, initCall); 18 | 19 | nft = ExampleNFT(proxy); 20 | } 21 | 22 | function integrationTest() internal view { 23 | require(nft.owner() == msg.sender); 24 | 25 | require(keccak256(abi.encode(nft.name())) == keccak256(abi.encode("My NFT"))); 26 | require(keccak256(abi.encode(nft.symbol())) == keccak256(abi.encode("NFTX"))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/test/ExampleNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {ExampleSetupScript} from "../src/ExampleSetupScript.sol"; 7 | 8 | contract TestExampleNFT is ExampleSetupScript { 9 | function setUp() public { 10 | setUpContracts(); 11 | } 12 | 13 | function test_data() public view { 14 | require(nft.owner() == address(this)); 15 | 16 | require(keccak256(abi.encode(nft.name())) == keccak256(abi.encode("My NFT"))); 17 | require(keccak256(abi.encode(nft.symbol())) == keccak256(abi.encode("NFTX"))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /exampleOZ/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | bytecode_hash = "none" 3 | fs_permissions = [{ access = "read-write", path = "./" }] 4 | -------------------------------------------------------------------------------- /exampleOZ/remappings.txt: -------------------------------------------------------------------------------- 1 | UDS/=lib/UDS/src/ 2 | upgrade-scripts/=../src/ 3 | forge-std/=lib/forge-std/src/ 4 | ds-test/=lib/forge-std/lib/ds-test/src/ 5 | openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ 6 | openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ 7 | -------------------------------------------------------------------------------- /exampleOZ/script/deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../src/ExampleSetupScript.sol"; 5 | 6 | /* 7 | 1. Start anvil: 8 | ``` 9 | anvil 10 | ``` 11 | 12 | 2. Simulate dry-run on anvil: 13 | ``` 14 | US_DRY_RUN=true forge script deploy --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --ffi 15 | ``` 16 | 17 | 3. Broadcast transactions to anvil: 18 | ``` 19 | forge script deploy --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvv --broadcast --ffi 20 | ``` 21 | 22 | Running the above script again will have no effect as long as state on anvil persists. 23 | If anvil is reset, you'll have to run the above commands with `US_RESET=true` in order to ignore the file containing invalid deployments. 24 | 25 | **/ 26 | 27 | contract deploy is ExampleSetupScript { 28 | function run() external { 29 | // uncommenting this line would mark the two contracts as having a compatible storage layout 30 | // isUpgradeSafe[31337][0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0][0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9] = true; // prettier-ignore 31 | 32 | // uncomment with current timestamp to confirm deployments on mainnet for 15 minutes or always allow via (block.timestamp) 33 | // mainnetConfirmation = 1667499028; 34 | 35 | // will run `vm.startBroadcast();` if ffi is enabled 36 | // ffi is required for running storage layout compatibility checks 37 | // if ffi is disabled, it will enter "dry-run" and 38 | // run `vm.startPrank(tx.origin)` instead for the script to be consistent 39 | upgradeScriptsBroadcast(); 40 | 41 | // run the setup scripts 42 | setUpContracts(); 43 | 44 | // we don't need broadcast from here on 45 | tryStopBroadcast(); 46 | 47 | // run an "integration test" 48 | integrationTest(); 49 | 50 | // console.log and store these in `deployments/{chainid}/deploy-latest.json` (if not in dry-run) 51 | storeLatestDeployments(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /exampleOZ/script/mint.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../src/ExampleSetupScript.sol"; 5 | 6 | /* 7 | # Anvil Dry-Run (make sure it is running): 8 | US_DRY_RUN=true forge script mint --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvvv --ffi 9 | 10 | # Broadcast: 11 | forge script mint --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvv --broadcast --ffi*/ 12 | 13 | contract mint is ExampleSetupScript { 14 | function setUpUpgradeScripts() internal override { 15 | // We only want to attach existing contracts. 16 | // Though if everything is up-to-date, this should be redundant and not needed. 17 | UPGRADE_SCRIPTS_ATTACH_ONLY = true; // disables re-deploying/upgrading 18 | 19 | // The following variables can all be set to change the behavior of the scripts. 20 | // These can also all be set through passing the argument in the command line 21 | // e.g: UPGRADE_SCRIPTS_RESET=true forge script ... 22 | 23 | // bool UPGRADE_SCRIPTS_RESET; // re-deploys all contracts 24 | // bool UPGRADE_SCRIPTS_BYPASS; // deploys contracts without any checks whatsoever 25 | // bool UPGRADE_SCRIPTS_DRY_RUN; // doesn't overwrite new deployments in deploy-latest.json 26 | // bool UPGRADE_SCRIPTS_ATTACH_ONLY; // doesn't deploy contracts, just attaches with checks 27 | } 28 | 29 | function run() external { 30 | // run the setup scripts; attach contracts 31 | setUpContracts(); 32 | 33 | upgradeScriptsBroadcast(); 34 | 35 | // do stuff 36 | nft.mint(msg.sender); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /exampleOZ/src/ExampleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {UUPSUpgradeable} from "openzeppelin-contracts/proxy/utils/UUPSUpgradeable.sol"; 5 | import {ERC721Upgradeable} from "openzeppelin-contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; 6 | import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | 8 | contract ExampleNFT is UUPSUpgradeable, ERC721Upgradeable, OwnableUpgradeable { 9 | uint256 totalSupply; 10 | uint256 public immutable version; 11 | 12 | // uint256 public contractId = 1; 13 | 14 | constructor(uint256 version_) { 15 | version = version_; 16 | _disableInitializers(); 17 | } 18 | 19 | function init(string memory name, string memory symbol) external initializer { 20 | __Ownable_init(); 21 | __ERC721_init(name, symbol); 22 | } 23 | 24 | function tokenURI(uint256) public pure override returns (string memory uri) { 25 | // uri = "abcd"; 26 | } 27 | 28 | function mint(address to) public { 29 | _mint(to, ++totalSupply); 30 | } 31 | 32 | function _authorizeUpgrade(address) internal override onlyOwner {} 33 | } 34 | -------------------------------------------------------------------------------- /exampleOZ/src/ExampleSetupScript.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ExampleNFT} from "./ExampleNFT.sol"; 5 | 6 | import {UpgradeScripts} from "upgrade-scripts/UpgradeScripts.sol"; 7 | import {ERC1967Proxy} from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; 8 | import {UUPSUpgradeable} from "openzeppelin-contracts/proxy/utils/UUPSUpgradeable.sol"; 9 | 10 | contract ExampleSetupScript is UpgradeScripts { 11 | ExampleNFT nft; 12 | 13 | /// @dev using OZ's ERC1967Proxy 14 | function getDeployProxyCode(address implementation, bytes memory initCall) 15 | internal 16 | pure 17 | override 18 | returns (bytes memory) 19 | { 20 | return abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(implementation, initCall)); 21 | } 22 | 23 | /// @dev using OZ's UUPSUpgradeable function call 24 | function upgradeProxy(address proxy, address newImplementation) internal override { 25 | UUPSUpgradeable(proxy).upgradeTo(newImplementation); 26 | } 27 | 28 | // /// @dev override using forge's built-in create2 deployer (only works for specific chains, or: use your own!) 29 | // function deployCode(bytes memory code) internal override returns (address addr) { 30 | // uint256 salt = 0x1234; 31 | 32 | // assembly { 33 | // addr := create2(0, add(code, 0x20), mload(code), salt) 34 | // } 35 | // } 36 | 37 | function setUpContracts() internal { 38 | // encodes constructor call: `ExampleNFT(1)` 39 | bytes memory constructorArgs = abi.encode(uint256(1)); 40 | address implementation = setUpContract("ExampleNFT", constructorArgs); 41 | 42 | // encodes function call: `ExampleNFT.init("My NFT", "NFTX")` 43 | bytes memory initCall = abi.encodeCall(ExampleNFT.init, ("My NFT", "NFTX")); 44 | address proxy = setUpProxy(implementation, initCall); 45 | 46 | nft = ExampleNFT(proxy); 47 | } 48 | 49 | function integrationTest() internal view { 50 | require(nft.owner() == msg.sender); 51 | 52 | require(keccak256(abi.encode(nft.name())) == keccak256(abi.encode("My NFT"))); 53 | require(keccak256(abi.encode(nft.symbol())) == keccak256(abi.encode("NFTX"))); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /exampleOZ/test/ExampleNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {ExampleSetupScript} from "../src/ExampleSetupScript.sol"; 7 | 8 | contract TestExampleNFT is ExampleSetupScript { 9 | function setUpUpgradeScripts() internal override { 10 | UPGRADE_SCRIPTS_BYPASS = true; // deploys contracts without any checks whatsoever 11 | } 12 | 13 | function setUp() public { 14 | setUpContracts(); 15 | } 16 | 17 | function test_integration() public view { 18 | require(nft.owner() == address(this)); 19 | 20 | require(keccak256(abi.encode(nft.name())) == keccak256(abi.encode("My NFT"))); 21 | require(keccak256(abi.encode(nft.symbol())) == keccak256(abi.encode("NFTX"))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | 3 | solc = "0.8.17" 4 | optimizer = true 5 | optimizer_runs = 1_000_000 6 | bytecode_hash = "none" 7 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | UDS/=lib/UDS/src/ 2 | forge-std/=lib/forge-std/src/ 3 | -------------------------------------------------------------------------------- /src/UpgradeScripts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import {UUPSUpgrade} from "UDS/proxy/UUPSUpgrade.sol"; 7 | import {LibEnumerableSet} from "UDS/lib/LibEnumerableSet.sol"; 8 | import {ERC1967Proxy, ERC1967_PROXY_STORAGE_SLOT} from "UDS/proxy/ERC1967Proxy.sol"; 9 | 10 | /// @title Foundry Upgrade Scripts 11 | /// @author 0xPhaze (https://github.com/0xPhaze/upgrade-scripts) 12 | /// @notice Scripts for setting up and keeping track of deployments & proxies 13 | contract UpgradeScripts is Script { 14 | using LibEnumerableSet for LibEnumerableSet.Uint256Set; 15 | 16 | struct ContractData { 17 | string key; 18 | address addr; 19 | } 20 | 21 | uint256 mainnetConfirmation; // last confirmation timestamp; must to be within `UPGRADE_SCRIPTS_CONFIRM_TIME_WINDOW` 22 | uint256 UPGRADE_SCRIPTS_CONFIRM_TIME_WINDOW = 15 minutes; 23 | 24 | bool UPGRADE_SCRIPTS_RESET; // re-deploys all contracts 25 | bool UPGRADE_SCRIPTS_BYPASS; // deploys contracts without any checks whatsoever 26 | bool UPGRADE_SCRIPTS_DRY_RUN; // doesn't overwrite new deployments in deploy-latest.json 27 | bool UPGRADE_SCRIPTS_ATTACH_ONLY; // doesn't deploy contracts, just attaches with checks 28 | bool UPGRADE_SCRIPTS_BYPASS_SAFETY; // bypass all upgrade safety checks 29 | 30 | // mappings chainid => ... 31 | mapping(uint256 => mapping(address => bool)) firstTimeDeployed; // set to true for contracts that are just deployed; useful for inits 32 | mapping(uint256 => mapping(address => mapping(address => bool))) isUpgradeSafe; // whether a contract => contract is deemed upgrade safe 33 | 34 | LibEnumerableSet.Uint256Set registeredChainIds; // chainids that contain registered contracts 35 | mapping(uint256 => ContractData[]) registeredContracts; // contracts registered through `setUpContract` or `setUpProxy` 36 | mapping(uint256 => mapping(address => string)) registeredContractName; // chainid => address => name mapping 37 | mapping(uint256 => mapping(string => address)) registeredContractAddress; // chainid => key => address mapping 38 | 39 | // cache for operations 40 | mapping(string => bool) __madeDir; 41 | mapping(uint256 => bool) __latestDeploymentsLoaded; 42 | mapping(uint256 => string) __latestDeploymentsJson; 43 | mapping(uint256 => mapping(address => bool)) __storageLayoutGenerated; 44 | 45 | constructor() { 46 | setUpUpgradeScripts(); // allows for environment variables to be set before initial load 47 | 48 | loadEnvVars(); 49 | 50 | if (UPGRADE_SCRIPTS_BYPASS) return; // bypass any checks 51 | if (UPGRADE_SCRIPTS_ATTACH_ONLY) return; // bypass any further checks (doesn't require FFI) 52 | 53 | // enforce dry-run when ffi is disabled, as otherwise 54 | // deployments won't be able to be stored in `deploy-latest.json` 55 | if (!UPGRADE_SCRIPTS_DRY_RUN && !isFFIEnabled()) { 56 | UPGRADE_SCRIPTS_DRY_RUN = true; 57 | 58 | console.log("Dry-run enabled (`FFI=false`)."); 59 | } 60 | } 61 | 62 | /* ------------- setUp ------------- */ 63 | 64 | /// @dev allows for `UPGRADE_SCRIPTS_*` variables to be set in override 65 | function setUpUpgradeScripts() internal virtual {} 66 | 67 | /// @notice Sets-up a contract. If a previous deployment is found, 68 | /// the creation-code-hash is checked against the stored contract's 69 | /// hash and a new contract is deployed if it is outdated or no 70 | /// previous deployment was found. Otherwise, it is simply attached. 71 | /// @param contractName name of the contract to be deployed (must be exact) 72 | /// @param constructorArgs abi-encoded constructor arguments 73 | /// @param key unique identifier to be used in logs 74 | /// @return implementation deployed or loaded contract implementation 75 | function setUpContract(string memory contractName, bytes memory constructorArgs, string memory key, bool attachOnly) 76 | internal 77 | virtual 78 | returns (address implementation) 79 | { 80 | string memory keyOrContractName = bytes(key).length == 0 ? contractName : key; 81 | bytes memory creationCode = abi.encodePacked(getContractCode(contractName), constructorArgs); 82 | 83 | if (UPGRADE_SCRIPTS_BYPASS) { 84 | implementation = deployCodeWrapper(creationCode); 85 | 86 | vm.label(implementation, keyOrContractName); 87 | 88 | return implementation; 89 | } 90 | if (UPGRADE_SCRIPTS_ATTACH_ONLY) attachOnly = true; 91 | 92 | bool deployNew = UPGRADE_SCRIPTS_RESET; 93 | 94 | if (!deployNew) { 95 | implementation = loadLatestDeployedAddress(keyOrContractName); 96 | 97 | if (implementation != address(0)) { 98 | if (implementation.code.length == 0) { 99 | console.log("Stored %s does not contain code.", contractLabel(contractName, implementation, key)); 100 | console.log( 101 | "Make sure '%s' contains all the latest deployments.", getDeploymentsPath("deploy-latest.json") 102 | ); // prettier-ignore 103 | 104 | throwError("Invalid contract address."); 105 | } 106 | 107 | if (creationCodeHashMatches(implementation, keccak256(creationCode))) { 108 | console.log("%s up-to-date.", contractLabel(contractName, implementation, key)); 109 | } else { 110 | console.log("Implementation for %s changed.", contractLabel(contractName, implementation, key)); 111 | 112 | if (attachOnly) console.log("Keeping existing deployment (`attachOnly=true`)."); 113 | else deployNew = true; 114 | } 115 | } else { 116 | console.log( 117 | "Existing implementation for %s not found.", contractLabel(contractName, implementation, key) 118 | ); // prettier-ignore 119 | 120 | deployNew = true; 121 | 122 | if (UPGRADE_SCRIPTS_ATTACH_ONLY) throwError("Contract deployment is missing."); 123 | } 124 | } 125 | 126 | if (deployNew) { 127 | implementation = confirmDeployCode(creationCode); 128 | 129 | console.log("=> new %s.\n", contractLabel(contractName, implementation, key)); 130 | 131 | saveCreationCodeHash(implementation, keccak256(creationCode)); 132 | } 133 | 134 | registerContract(keyOrContractName, contractName, implementation); 135 | } 136 | 137 | /// @notice Sets-up a proxy. If a previous deployment is found, 138 | /// it makes sure that the stored implementation matches the 139 | /// current one. Includes checks for whether the implementation 140 | /// is upgrade-compatible. If performing an upgrade, storage 141 | /// layout is diff-checked. Throws if any changes are present. 142 | /// @param implementation address of the contract for delegatecalls 143 | /// @param initCall abi-encoded arguments for an initial delegatecall to be 144 | /// performed during the contract's deployment 145 | /// @param key unique identifier to be used in logs 146 | /// @return proxy deployed or loaded proxy address 147 | function setUpProxy(address implementation, bytes memory initCall, string memory key, bool attachOnly) 148 | internal 149 | virtual 150 | returns (address proxy) 151 | { 152 | string memory contractName = registeredContractName[block.chainid][implementation]; 153 | string memory keyOrContractName = bytes(key).length == 0 ? string.concat(contractName, "Proxy") : key; 154 | 155 | // always run this check, as otherwise the error-message is confusing 156 | assertIsERC1967Upgrade(implementation, keyOrContractName); 157 | 158 | if (UPGRADE_SCRIPTS_BYPASS) { 159 | proxy = deployProxy(implementation, initCall); 160 | 161 | vm.label(proxy, keyOrContractName); 162 | 163 | return proxy; 164 | } 165 | if (UPGRADE_SCRIPTS_ATTACH_ONLY) attachOnly = true; 166 | 167 | // we require the contract name/type to be able to create a storage layout mapping 168 | // for the implementation tied to this proxy's address 169 | if (bytes(contractName).length == 0) { 170 | console.log( 171 | "Could not identify proxy contract name/type for implementation %s [key: %s].", implementation, key 172 | ); // prettier-ignore 173 | console.log( 174 | "Make sure the implementation type was set up via `setUpContract` for its type to be registered." 175 | ); // prettier-ignore 176 | console.log( 177 | 'Otherwise it can be set explicitly by adding `registeredContractName[%s] = "MyContract";`.', 178 | implementation 179 | ); // prettier-ignore 180 | 181 | throwError("Could not identify contract type."); 182 | } 183 | 184 | bool deployNew = UPGRADE_SCRIPTS_RESET; 185 | 186 | if (!deployNew) { 187 | proxy = loadLatestDeployedAddress(keyOrContractName); 188 | 189 | if (proxy != address(0)) { 190 | address storedImplementation = 191 | loadProxyStoredImplementation(proxy, proxyLabel(proxy, contractName, address(0), key)); 192 | 193 | if (storedImplementation != implementation) { 194 | console.log( 195 | "Existing %s needs upgrade.", proxyLabel(proxy, contractName, storedImplementation, key) 196 | ); // prettier-ignore 197 | 198 | if (attachOnly) { 199 | console.log("Keeping existing deployment (`attachOnly=true`)."); 200 | } else { 201 | upgradeSafetyChecks(contractName, storedImplementation, implementation); 202 | 203 | console.log("Upgrading %s.\n", proxyLabel(proxy, contractName, implementation, key)); 204 | 205 | confirmUpgradeProxy(proxy, implementation); 206 | } 207 | } else { 208 | console.log("%s up-to-date.", proxyLabel(proxy, contractName, implementation, key)); 209 | } 210 | } else { 211 | console.log("Existing %s not found.", proxyLabel(proxy, contractName, implementation, key)); 212 | 213 | if (UPGRADE_SCRIPTS_ATTACH_ONLY) throwError("Contract deployment is missing."); 214 | 215 | deployNew = true; 216 | } 217 | } 218 | 219 | if (deployNew) { 220 | proxy = confirmDeployProxy(implementation, initCall); 221 | 222 | console.log("=> new %s.\n", proxyLabel(proxy, contractName, implementation, key)); 223 | 224 | generateStorageLayoutFile(contractName, implementation); 225 | } 226 | 227 | registerContract(keyOrContractName, contractName, proxy); 228 | } 229 | 230 | /* ------------- overloads ------------- */ 231 | 232 | function setUpContract(string memory contractName, bytes memory constructorArgs, string memory key) 233 | internal 234 | virtual 235 | returns (address) 236 | { 237 | return setUpContract(contractName, constructorArgs, key, false); 238 | } 239 | 240 | function setUpContract(string memory contractName) internal virtual returns (address) { 241 | return setUpContract(contractName, "", "", false); 242 | } 243 | 244 | function setUpContract(string memory contractName, bytes memory constructorArgs) 245 | internal 246 | virtual 247 | returns (address) 248 | { 249 | return setUpContract(contractName, constructorArgs, "", false); 250 | } 251 | 252 | function setUpProxy(address implementation, bytes memory initCall, string memory key) 253 | internal 254 | virtual 255 | returns (address) 256 | { 257 | return setUpProxy(implementation, initCall, key, false); 258 | } 259 | 260 | function setUpProxy(address implementation, bytes memory initCall) internal virtual returns (address) { 261 | return setUpProxy(implementation, initCall, "", false); 262 | } 263 | 264 | function setUpProxy(address implementation) internal virtual returns (address) { 265 | return setUpProxy(implementation, "", "", false); 266 | } 267 | 268 | /* ------------- snippets ------------- */ 269 | 270 | function loadEnvVars() internal virtual { 271 | // silently bypass everything if set in the scripts 272 | if (!UPGRADE_SCRIPTS_BYPASS) { 273 | UPGRADE_SCRIPTS_RESET = tryLoadEnvBool(UPGRADE_SCRIPTS_RESET, "UPGRADE_SCRIPTS_RESET", "US_RESET"); 274 | UPGRADE_SCRIPTS_BYPASS = tryLoadEnvBool(UPGRADE_SCRIPTS_BYPASS, "UPGRADE_SCRIPTS_BYPASS", "US_BYPASS"); 275 | UPGRADE_SCRIPTS_DRY_RUN = tryLoadEnvBool(UPGRADE_SCRIPTS_DRY_RUN, "UPGRADE_SCRIPTS_DRY_RUN", "US_DRY_RUN"); 276 | UPGRADE_SCRIPTS_ATTACH_ONLY = 277 | tryLoadEnvBool(UPGRADE_SCRIPTS_ATTACH_ONLY, "UPGRADE_SCRIPTS_ATTACH_ONLY", "US_ATTACH_ONLY"); // prettier-ignore 278 | UPGRADE_SCRIPTS_BYPASS_SAFETY = 279 | tryLoadEnvBool(UPGRADE_SCRIPTS_BYPASS_SAFETY, "UPGRADE_SCRIPTS_BYPASS_SAFETY", "US_BYPASS_SAFETY"); // prettier-ignore 280 | 281 | if ( 282 | UPGRADE_SCRIPTS_RESET || UPGRADE_SCRIPTS_BYPASS || UPGRADE_SCRIPTS_DRY_RUN 283 | || UPGRADE_SCRIPTS_ATTACH_ONLY || UPGRADE_SCRIPTS_BYPASS_SAFETY 284 | ) console.log(""); 285 | } 286 | } 287 | 288 | function tryLoadEnvBool(bool defaultVal, string memory varName, string memory varAlias) 289 | internal 290 | virtual 291 | returns (bool val) 292 | { 293 | val = defaultVal; 294 | 295 | if (!val) { 296 | try vm.envBool(varName) returns (bool val_) { 297 | val = val_; 298 | } catch { 299 | try vm.envBool(varAlias) returns (bool val_) { 300 | val = val_; 301 | } catch {} 302 | } 303 | } 304 | 305 | if (val) console.log("%s=true", varName); 306 | } 307 | 308 | function startBroadcastIfNotDryRun() internal { 309 | upgradeScriptsBroadcast(address(0)); 310 | } 311 | 312 | function upgradeScriptsBroadcast() internal { 313 | upgradeScriptsBroadcast(address(0)); 314 | } 315 | 316 | function upgradeScriptsBroadcast(address account) internal { 317 | if (!UPGRADE_SCRIPTS_DRY_RUN) { 318 | if (account != address(0)) vm.startBroadcast(account); 319 | else vm.startBroadcast(); 320 | } else { 321 | console.log("Disabling `vm.broadcast` (dry-run).\n"); 322 | console.log("Make sure you are running this without `--broadcast`."); 323 | 324 | tryStopBroadcast(); 325 | 326 | // need to start prank instead now to be consistent in "dry-run" 327 | vm.startPrank(tx.origin); 328 | } 329 | } 330 | 331 | function loadLatestDeployedAddress(string memory key) internal virtual returns (address) { 332 | return loadLatestDeployedAddress(key, block.chainid); 333 | } 334 | 335 | function loadLatestDeployedAddress(string memory key, uint256 chainId) internal virtual returns (address) { 336 | if (!__latestDeploymentsLoaded[chainId]) { 337 | try vm.readFile(getDeploymentsPath("deploy-latest.json", chainId)) returns (string memory json) { 338 | __latestDeploymentsJson[chainId] = json; 339 | } catch {} 340 | __latestDeploymentsLoaded[chainId] = true; 341 | } 342 | 343 | if (bytes(__latestDeploymentsJson[chainId]).length != 0) { 344 | try vm.parseJson(__latestDeploymentsJson[chainId], string.concat(".", key)) returns (bytes memory data) { 345 | if (data.length == 32) return abi.decode(data, (address)); 346 | } catch {} 347 | } 348 | 349 | return address(0); 350 | } 351 | 352 | function loadProxyStoredImplementation(address proxy) internal virtual returns (address) { 353 | return loadProxyStoredImplementation(proxy, ""); 354 | } 355 | 356 | function loadProxyStoredImplementation(address proxy, string memory label) 357 | internal 358 | virtual 359 | returns (address implementation) 360 | { 361 | require(proxy.code.length != 0, string.concat("No code stored at ", label, ".")); 362 | 363 | try vm.load(proxy, ERC1967_PROXY_STORAGE_SLOT) returns (bytes32 data) { 364 | implementation = address(uint160(uint256(data))); 365 | 366 | // note: proxies should never have implementation address(0) stored 367 | require( 368 | implementation != address(0), 369 | string.concat("Invalid existing implementation address(0) stored in ", label, ".") 370 | ); 371 | require( 372 | UUPSUpgrade(implementation).proxiableUUID() == ERC1967_PROXY_STORAGE_SLOT, 373 | string.concat( 374 | "Proxy ", 375 | label, 376 | " trying to upgrade to implementation with invalid proxiable UUID: ", 377 | vm.toString(implementation) 378 | ) // prettier-ignore 379 | ); 380 | } catch { 381 | // won't happen 382 | console.log("Contract %s not identified as a proxy", proxy); 383 | } 384 | } 385 | 386 | function generateStorageLayoutFile(string memory contractName, address implementation) internal virtual { 387 | if (__storageLayoutGenerated[block.chainid][implementation]) return; 388 | 389 | if (!isFFIEnabled()) { 390 | return console.log( 391 | "SKIPPING storage layout mapping for %s (`FFI=false`).\n", 392 | contractLabel(contractName, implementation, "") 393 | ); // prettier-ignore 394 | } 395 | 396 | console.log("Generating storage layout mapping for %s.\n", contractLabel(contractName, implementation, "")); 397 | 398 | // assert Contract exists 399 | getContractCode(contractName); 400 | 401 | // mkdir if not already 402 | mkdir(getDeploymentsPath("data/")); 403 | 404 | string[] memory script = new string[](4); 405 | script[0] = "forge"; 406 | script[1] = "inspect"; 407 | script[2] = contractName; 408 | script[3] = "storage-layout"; 409 | 410 | bytes memory out = vm.ffi(script); 411 | 412 | vm.writeFile(getStorageLayoutFilePath(implementation), string(out)); 413 | 414 | __storageLayoutGenerated[block.chainid][implementation] = true; 415 | } 416 | 417 | function upgradeSafetyChecks(string memory contractName, address oldImplementation, address newImplementation) 418 | internal 419 | virtual 420 | { 421 | // note that `assertIsERC1967Upgrade(newImplementation);` is always run beforehand in any case 422 | 423 | if (!isFFIEnabled()) { 424 | return console.log( 425 | "SKIPPING storage layout compatibility check [%s <-> %s] (`FFI=false`).", 426 | oldImplementation, 427 | newImplementation 428 | ); // prettier-ignore 429 | } 430 | 431 | generateStorageLayoutFile(contractName, newImplementation); 432 | 433 | if (UPGRADE_SCRIPTS_BYPASS_SAFETY) { 434 | return console.log( 435 | "\nWARNING: Bypassing storage layout compatibility check [%s <-> %s] (`UPGRADE_SCRIPTS_BYPASS_SAFETY=true`).", 436 | oldImplementation, 437 | newImplementation 438 | ); // prettier-ignore 439 | } 440 | if (isUpgradeSafe[block.chainid][oldImplementation][newImplementation]) { 441 | return console.log( 442 | "Storage layout compatibility check [%s <-> %s]: Pass (`isUpgradeSafe=true`)", 443 | oldImplementation, 444 | newImplementation 445 | ); // prettier-ignore 446 | } 447 | 448 | // @note give hint to skip via `isUpgradeSafe`? 449 | assertFileExists(getStorageLayoutFilePath(oldImplementation)); 450 | assertFileExists(getStorageLayoutFilePath(newImplementation)); 451 | 452 | string[] memory script = new string[](8); 453 | 454 | script[0] = "diff"; 455 | script[1] = "-ayw"; 456 | script[2] = "-W"; 457 | script[3] = "180"; 458 | script[4] = "--side-by-side"; 459 | script[5] = "--suppress-common-lines"; 460 | script[6] = getStorageLayoutFilePath(oldImplementation); 461 | script[7] = getStorageLayoutFilePath(newImplementation); 462 | 463 | bytes memory diff = vm.ffi(script); 464 | 465 | if (diff.length == 0) { 466 | console.log("Storage layout compatibility check [%s <-> %s]: Pass.", oldImplementation, newImplementation); 467 | } else { 468 | console.log("Storage layout compatibility check [%s <-> %s]: Fail", oldImplementation, newImplementation); 469 | console.log("\n%s Diff:", contractName); 470 | console.log(string(diff)); 471 | 472 | console.log( 473 | "\nIf you believe the storage layout is compatible, add the following to the beginning of `run()` in your deploy script.\n```" 474 | ); // prettier-ignore 475 | console.log("isUpgradeSafe[%s][%s][%s] = true;\n```", block.chainid, oldImplementation, newImplementation); // prettier-ignore 476 | 477 | throwError("Contract storage layout changed and might not be compatible."); 478 | } 479 | 480 | isUpgradeSafe[block.chainid][oldImplementation][newImplementation] = true; 481 | } 482 | 483 | function saveCreationCodeHash(address addr, bytes32 creationCodeHash) internal virtual { 484 | if (UPGRADE_SCRIPTS_DRY_RUN) return; 485 | 486 | mkdir(getDeploymentsPath("data/")); 487 | 488 | string memory path = getCreationCodeHashFilePath(addr); 489 | 490 | vm.writeFile(path, vm.toString(creationCodeHash)); 491 | } 492 | 493 | /// @dev .codehash is an improper check for contracts that use immutables 494 | function creationCodeHashMatches(address addr, bytes32 newCreationCodeHash) internal virtual returns (bool) { 495 | string memory path = getCreationCodeHashFilePath(addr); 496 | 497 | try vm.readFile(path) returns (string memory data) { 498 | bytes32 codehash = vm.parseBytes32(data); 499 | 500 | return codehash == newCreationCodeHash; 501 | } catch {} 502 | 503 | return false; 504 | } 505 | 506 | function fileExists(string memory file) internal virtual returns (bool exists) { 507 | string[] memory script = new string[](2); 508 | script[0] = "ls"; 509 | script[1] = file; 510 | 511 | try vm.ffi(script) returns (bytes memory res) { 512 | if (bytes(res).length != 0) { 513 | exists = true; 514 | } 515 | } catch {} 516 | } 517 | 518 | function assertFileExists(string memory file) internal virtual { 519 | if (!fileExists(file)) { 520 | console.log("Unable to locate file '%s'.", file); 521 | console.log("You can bypass storage layout comparisons by setting `isUpgradeSafe[..] = true;`."); 522 | 523 | throwError("File does not exist."); 524 | } 525 | } 526 | 527 | function assertIsERC1967Upgrade(address implementation) internal virtual { 528 | assertIsERC1967Upgrade(implementation, ""); 529 | } 530 | 531 | function assertIsERC1967Upgrade(address implementation, string memory contractName) internal virtual { 532 | if (implementation.code.length == 0) { 533 | console.log("No code stored at %s(%s).", contractName, implementation); 534 | 535 | throwError("Invalid contract address."); 536 | } 537 | 538 | try UUPSUpgrade(implementation).proxiableUUID() returns (bytes32 uuid) { 539 | if (uuid != ERC1967_PROXY_STORAGE_SLOT) { 540 | console.log("Invalid proxiable UUID for implementation %s(%s).", contractName, implementation); 541 | 542 | throwError("Contract not upgradeable."); 543 | } 544 | } catch { 545 | console.log("Contract %s(%s) does not implement proxiableUUID().", contractName, implementation); 546 | 547 | throwError("Contract not upgradeable."); 548 | } 549 | } 550 | 551 | function getContractCode(string memory contractName) internal virtual returns (bytes memory code) { 552 | bytes memory reason; 553 | 554 | try vm.getCode(contractName) returns (bytes memory code_) { 555 | code = code_; 556 | } catch (bytes memory reason_) { 557 | reason = reason_; 558 | try vm.getCode(string.concat(contractName, ".sol:", contractName)) returns (bytes memory code_) { 559 | code = code_; 560 | } catch {} 561 | } 562 | 563 | if (code.length == 0) { 564 | console.log("Unable to find artifact '%s'.", contractName); 565 | console.log("Provide either a unique contract name 'MyContract',"); 566 | console.log("or an artifact location 'MyContract.sol:MyContract'."); 567 | 568 | if (reason.length != 0) { 569 | assembly { 570 | revert(add(0x20, reason), mload(reason)) 571 | } 572 | } 573 | 574 | throwError("Contract does not exist."); 575 | } 576 | } 577 | 578 | /// @dev code for constructing ERC1967Proxy(address implementation, bytes memory initCall) 579 | /// makes an initial delegatecall to `implementation` with calldata `initCall` (if `initCall` != "") 580 | function getDeployProxyCode(address implementation, bytes memory initCall) 581 | internal 582 | view 583 | virtual 584 | returns (bytes memory) 585 | { 586 | this; 587 | return abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(implementation, initCall)); 588 | } 589 | 590 | function requireConfirmation() internal virtual { 591 | if (isTestnet() || UPGRADE_SCRIPTS_DRY_RUN || UPGRADE_SCRIPTS_BYPASS) return; 592 | 593 | if (block.timestamp - mainnetConfirmation > UPGRADE_SCRIPTS_CONFIRM_TIME_WINDOW) { 594 | revert( 595 | string.concat( 596 | "MAINNET CONFIRMATION REQUIRED: \n```\nmainnetConfirmation = ", 597 | vm.toString(block.timestamp), 598 | ";\n```" 599 | ) 600 | ); 601 | } 602 | } 603 | 604 | function confirmUpgradeProxy(address proxy, address newImplementation) internal virtual { 605 | requireConfirmation(); 606 | 607 | upgradeProxy(proxy, newImplementation); 608 | } 609 | 610 | function upgradeProxy(address proxy, address newImplementation) internal virtual { 611 | UUPSUpgrade(proxy).upgradeToAndCall(newImplementation, ""); 612 | } 613 | 614 | function confirmDeployProxy(address implementation, bytes memory initCall) internal virtual returns (address) { 615 | return confirmDeployCode(getDeployProxyCode(implementation, initCall)); 616 | } 617 | 618 | function confirmDeployCode(bytes memory code) internal virtual returns (address) { 619 | requireConfirmation(); 620 | 621 | return deployCodeWrapper(code); 622 | } 623 | 624 | function deployProxy(address implementation, bytes memory initCall) internal virtual returns (address) { 625 | return deployCodeWrapper(getDeployProxyCode(implementation, initCall)); 626 | } 627 | 628 | function deployCodeWrapper(bytes memory code) internal virtual returns (address addr) { 629 | addr = deployCode(code); 630 | 631 | firstTimeDeployed[block.chainid][addr] = true; 632 | 633 | require(addr.code.length != 0, string.concat("Failed to deploy code.", vm.toString(code))); 634 | } 635 | 636 | /* ------------- utils ------------- */ 637 | 638 | function tryStopBroadcast() internal { 639 | try vm.stopBroadcast() {} catch (bytes memory) {} 640 | } 641 | 642 | function isFirstTimeDeployed(address addr) internal virtual returns (bool) { 643 | return firstTimeDeployed[block.chainid][addr]; 644 | } 645 | 646 | function deployCode(bytes memory code) internal virtual returns (address addr) { 647 | assembly { 648 | addr := create(0, add(code, 0x20), mload(code)) 649 | } 650 | } 651 | 652 | function hasCode(address addr) internal view virtual returns (bool hasCode_) { 653 | assembly { 654 | hasCode_ := iszero(iszero(extcodesize(addr))) 655 | } 656 | } 657 | 658 | function isTestnet() internal view virtual returns (bool) { 659 | uint256 chainId = block.chainid; 660 | 661 | if (chainId == 4) return true; // Rinkeby 662 | if (chainId == 5) return true; // Goerli 663 | if (chainId == 97) return true; // BSC Testnet 664 | if (chainId == 420) return true; // Optimism 665 | if (chainId == 31_337) return true; // Anvil 666 | if (chainId == 31_338) return true; // Anvil 667 | if (chainId == 80_001) return true; // Mumbai 668 | if (chainId == 421_611) return true; // Arbitrum 669 | if (chainId == 11_155_111) return true; // Sepolia 670 | 671 | return false; 672 | } 673 | 674 | function isFFIEnabled() internal virtual returns (bool) { 675 | string[] memory script = new string[](1); 676 | script[0] = "echo"; 677 | 678 | try vm.ffi(script) { 679 | return true; 680 | } catch { 681 | return false; 682 | } 683 | } 684 | 685 | function mkdir(string memory path) internal virtual { 686 | if (__madeDir[path]) return; 687 | 688 | string[] memory script = new string[](3); 689 | script[0] = "mkdir"; 690 | script[1] = "-p"; 691 | script[2] = path; 692 | 693 | vm.ffi(script); 694 | 695 | __madeDir[path] = true; 696 | } 697 | 698 | /// @dev throwing error like this, because sometimes foundry won't display any logs otherwise 699 | function throwError(string memory message) internal view { 700 | if (bytes(message).length != 0) console.log("\nError: %s", message); 701 | 702 | // Must revert if not dry run to cancel broadcasting transactions. 703 | if (!UPGRADE_SCRIPTS_DRY_RUN) { 704 | revert( 705 | string.concat( 706 | message, "\nEnable dry-run (`UPGRADE_SCRIPTS_DRY_RUN=true`) if the error message did not show." 707 | ) 708 | ); 709 | } // prettier-ignore 710 | 711 | // Sometimes Forge does not display the complete message then.. 712 | // That's why we return instead. 713 | assembly { 714 | return(0, 0) 715 | } 716 | } 717 | 718 | /* ------------- contract registry ------------- */ 719 | 720 | function registerContract(string memory key, string memory name, address addr) internal virtual { 721 | uint256 chainId = block.chainid; 722 | 723 | if (registeredContractAddress[chainId][key] != address(0)) { 724 | console.log("Duplicate entry for key [%s] found when registering contract.", key); 725 | console.log("Found: %s(%s)", key, registeredContractAddress[chainId][key]); 726 | 727 | throwError("Duplicate key."); 728 | } 729 | 730 | registeredChainIds.add(chainId); 731 | 732 | registeredContractName[chainId][addr] = name; 733 | registeredContractAddress[chainId][key] = addr; 734 | 735 | registeredContracts[chainId].push(ContractData({key: key, addr: addr})); 736 | 737 | vm.label(addr, key); 738 | } 739 | 740 | function logDeployments() internal view virtual { 741 | title("Registered Contracts"); 742 | 743 | for (uint256 i; i < registeredChainIds.length(); i++) { 744 | uint256 chainId = registeredChainIds.at(i); 745 | 746 | console.log("Chain id %s:\n", chainId); 747 | for (uint256 j; j < registeredContracts[chainId].length; j++) { 748 | console.log("%s=%s", registeredContracts[chainId][j].key, registeredContracts[chainId][j].addr); 749 | } 750 | 751 | console.log(""); 752 | } 753 | } 754 | 755 | function generateRegisteredContractsJson(uint256 chainId) internal virtual returns (string memory json) { 756 | if (registeredContracts[chainId].length == 0) return ""; 757 | 758 | json = string.concat("{\n", ' "git-commit-hash": "', getGitCommitHash(), '",\n'); 759 | 760 | for (uint256 i; i < registeredContracts[chainId].length; i++) { 761 | json = string.concat( 762 | json, 763 | ' "', 764 | registeredContracts[chainId][i].key, 765 | '": "', 766 | vm.toString(registeredContracts[chainId][i].addr), 767 | i + 1 == registeredContracts[chainId].length ? '"\n' : '",\n' 768 | ); 769 | } 770 | json = string.concat(json, "}"); 771 | } 772 | 773 | function storeLatestDeployments() internal virtual { 774 | storeDeployments(); 775 | } 776 | 777 | function storeDeployments() internal virtual { 778 | logDeployments(); 779 | 780 | if (!UPGRADE_SCRIPTS_DRY_RUN) { 781 | for (uint256 i; i < registeredChainIds.length(); i++) { 782 | uint256 chainId = registeredChainIds.at(i); 783 | 784 | string memory json = generateRegisteredContractsJson(chainId); 785 | 786 | if (keccak256(bytes(json)) == keccak256(bytes(__latestDeploymentsJson[chainId]))) { 787 | console.log("No changes detected.", chainId); 788 | } else { 789 | mkdir(getDeploymentsPath("", chainId)); 790 | 791 | vm.writeFile(getDeploymentsPath(string.concat("deploy-latest.json"), chainId), json); 792 | vm.writeFile( 793 | getDeploymentsPath(string.concat("deploy-", vm.toString(block.timestamp), ".json"), chainId), 794 | json 795 | ); // prettier-ignore 796 | 797 | console.log("Deployments saved to %s.", getDeploymentsPath("deploy-latest.json", chainId)); // prettier-ignore 798 | } 799 | } 800 | } 801 | } 802 | 803 | function getGitCommitHash() internal virtual returns (string memory) { 804 | string[] memory script = new string[](3); 805 | script[0] = "git"; 806 | script[1] = "rev-parse"; 807 | script[2] = "HEAD"; 808 | 809 | bytes memory hash = vm.ffi(script); 810 | 811 | if (hash.length != 20) { 812 | console.log("Unable to get commit hash."); 813 | return ""; 814 | } 815 | 816 | string memory hashStr = vm.toString(hash); 817 | 818 | // remove the "0x" prefix 819 | assembly { 820 | mstore(add(hashStr, 2), sub(mload(hashStr), 2)) 821 | hashStr := add(hashStr, 2) 822 | } 823 | return hashStr; 824 | } 825 | 826 | /* ------------- filePath ------------- */ 827 | 828 | function getDeploymentsPath(string memory path) internal virtual returns (string memory) { 829 | return getDeploymentsPath(path, block.chainid); 830 | } 831 | 832 | function getDeploymentsPath(string memory path, uint256 chainId) internal virtual returns (string memory) { 833 | return string.concat("deployments/", vm.toString(chainId), "/", path); 834 | } 835 | 836 | function getCreationCodeHashFilePath(address addr) internal virtual returns (string memory) { 837 | return getDeploymentsPath(string.concat("data/", vm.toString(addr), ".creation-code-hash")); 838 | } 839 | 840 | function getStorageLayoutFilePath(address addr) internal virtual returns (string memory) { 841 | return getDeploymentsPath(string.concat("data/", vm.toString(addr), ".storage-layout")); 842 | } 843 | 844 | /* ------------- prints ------------- */ 845 | 846 | function title(string memory name) internal view virtual { 847 | console.log("\n=========================="); 848 | console.log("%s:\n", name); 849 | } 850 | 851 | function contractLabel(string memory contractName, address addr, string memory key) 852 | internal 853 | virtual 854 | returns (string memory) 855 | { 856 | return string.concat( 857 | contractName, 858 | addr == address(0) ? "" : string.concat("(", vm.toString(addr), ")"), 859 | bytes(key).length == 0 ? "" : string.concat(" [", key, "]") 860 | ); 861 | } 862 | 863 | function proxyLabel(address proxy, string memory contractName, address implementation, string memory key) 864 | internal 865 | virtual 866 | returns (string memory) 867 | { 868 | return string.concat( 869 | "Proxy::", 870 | contractName, 871 | proxy == address(0) 872 | ? "" 873 | : string.concat( 874 | "(", 875 | vm.toString(proxy), 876 | implementation == address(0) ? "" : string.concat(" -> ", vm.toString(implementation)), 877 | ")" 878 | ), 879 | bytes(key).length == 0 ? "" : string.concat(" [", key, "]") 880 | ); 881 | } 882 | } 883 | --------------------------------------------------------------------------------