Deployer.sol 17.1 KB
Newer Older
Mark Tyneway's avatar
Mark Tyneway committed
1
// SPDX-License-Identifier: MIT
2
pragma solidity ^0.8.0;
Mark Tyneway's avatar
Mark Tyneway committed
3 4 5 6

import { Script } from "forge-std/Script.sol";
import { stdJson } from "forge-std/StdJson.sol";
import { console2 as console } from "forge-std/console2.sol";
7
import { Executables } from "scripts/Executables.sol";
8 9 10
import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol";
import { IAddressManager } from "scripts/interfaces/IAddressManager.sol";
import { LibString } from "solady/utils/LibString.sol";
11
import { Artifacts, Deployment } from "scripts/Artifacts.s.sol";
Mark Tyneway's avatar
Mark Tyneway committed
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

/// @notice A `hardhat-deploy` style artifact
struct Artifact {
    string abi;
    address addr;
    string[] args;
    bytes bytecode;
    bytes deployedBytecode;
    string devdoc;
    string metadata;
    uint256 numDeployments;
    string receipt;
    bytes32 solcInputHash;
    string storageLayout;
    bytes32 transactionHash;
    string userdoc;
}

30 31 32 33 34 35 36 37 38 39 40
/// @notice Contains information about a storage slot. Mirrors the layout of the storage
///         slot object in Forge artifacts so that we can deserialize JSON into this struct.
struct StorageSlot {
    uint256 astId;
    string _contract;
    string label;
    uint256 offset;
    string slot;
    string _type;
}

Mark Tyneway's avatar
Mark Tyneway committed
41 42 43 44 45 46
/// @title Deployer
/// @author tynes
/// @notice A contract that can make deploying and interacting with deployments easy.
///         When a contract is deployed, call the `save` function to write its name and
///         contract address to disk. Then the `sync` function can be called to generate
///         hardhat deploy style artifacts. Forked from `forge-deploy`.
47
abstract contract Deployer is Script, Artifacts {
Mark Tyneway's avatar
Mark Tyneway committed
48 49
    /// @notice Path to the deploy artifact generated by foundry
    string internal deployPath;
50

Mark Tyneway's avatar
Mark Tyneway committed
51 52 53 54
    /// @notice The name of the deploy script that sends the transactions.
    ///         Can be modified with the env var DEPLOY_SCRIPT
    string internal deployScript;

55 56 57 58 59 60 61
    /// @notice Create the global variables and set up the filesystem.
    ///         Forge script will create a file where the prefix is the
    ///         name of the function that runs with the suffix `-latest.json`.
    ///         By default, `run()` is called. Allow the user to use the SIG
    ///         env var to specify what function signature was called so that
    ///         the `sync()` method can be used to create hardhat deploy style
    ///         artifacts.
62 63 64
    function setUp() public virtual override {
        Artifacts.setUp();

Mark Tyneway's avatar
Mark Tyneway committed
65
        deployScript = vm.envOr("DEPLOY_SCRIPT", name());
Mark Tyneway's avatar
Mark Tyneway committed
66

67
        string memory sig = vm.envOr("SIG", string("run"));
68
        string memory deployFile = vm.envOr("DEPLOY_FILE", string.concat(sig, "-latest.json"));
Mark Tyneway's avatar
Mark Tyneway committed
69
        uint256 chainId = vm.envOr("CHAIN_ID", block.chainid);
70 71 72
        deployPath = string.concat(
            vm.projectRoot(), "/broadcast/", deployScript, ".s.sol/", vm.toString(chainId), "/", deployFile
        );
Mark Tyneway's avatar
Mark Tyneway committed
73 74 75 76 77 78 79
    }

    /// @notice Call this function to sync the deployment artifacts such that
    ///         hardhat deploy style artifacts are created.
    function sync() public {
        Deployment[] memory deployments = _getTempDeployments();
        console.log("Syncing %s deployments", deployments.length);
80
        console.log("Using deployment artifact %s", deployPath);
Mark Tyneway's avatar
Mark Tyneway committed
81 82 83 84 85 86

        for (uint256 i; i < deployments.length; i++) {
            address addr = deployments[i].addr;
            string memory deploymentName = deployments[i].name;

            string memory deployTx = _getDeployTransactionByContractAddress(addr);
87 88 89 90
            if (bytes(deployTx).length == 0) {
                console.log("Deploy Tx not found for %s skipping deployment artifact generation", deploymentName);
                continue;
            }
91
            string memory contractName = _getContractNameFromDeployTransaction(deployTx);
92
            console.log("Syncing deployment %s: contract %s", deploymentName, contractName);
Mark Tyneway's avatar
Mark Tyneway committed
93 94

            string[] memory args = getDeployTransactionConstructorArguments(deployTx);
95 96
            bytes memory code = _getCode(contractName);
            bytes memory deployedCode = _getDeployedCode(contractName);
Mark Tyneway's avatar
Mark Tyneway committed
97 98 99 100 101 102 103 104
            string memory receipt = _getDeployReceiptByContractAddress(addr);

            string memory artifactPath = string.concat(deploymentsDir, "/", deploymentName, ".json");

            uint256 numDeployments = 0;
            try vm.readFile(artifactPath) returns (string memory res) {
                numDeployments = stdJson.readUint(string(res), "$.numDeployments");
                vm.removeFile(artifactPath);
105
            } catch { }
106
            numDeployments++;
Mark Tyneway's avatar
Mark Tyneway committed
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125

            Artifact memory artifact = Artifact({
                abi: getAbi(contractName),
                addr: addr,
                args: args,
                bytecode: code,
                deployedBytecode: deployedCode,
                devdoc: getDevDoc(contractName),
                metadata: getMetadata(contractName),
                numDeployments: numDeployments,
                receipt: receipt,
                solcInputHash: bytes32(0),
                storageLayout: getStorageLayout(contractName),
                transactionHash: stdJson.readBytes32(deployTx, "$.hash"),
                userdoc: getUserDoc(contractName)
            });

            string memory json = _serializeArtifact(artifact);

126
            vm.writeJson({ json: json, path: artifactPath });
Mark Tyneway's avatar
Mark Tyneway committed
127 128 129 130 131 132
        }

        console.log("Synced temp deploy files, deleting %s", tempDeploymentsPath);
        vm.removeFile(tempDeploymentsPath);
    }

Mark Tyneway's avatar
Mark Tyneway committed
133 134
    /// @notice Returns the name of the deployment script. Children contracts
    ///         must implement this to ensure that the deploy artifacts can be found.
135 136
    ///         This should be the same as the name of the script and is used as the file
    ///         name inside of the `broadcast` directory when looking up deployment artifacts.
137
    function name() public pure virtual returns (string memory);
Mark Tyneway's avatar
Mark Tyneway committed
138

Mark Tyneway's avatar
Mark Tyneway committed
139 140 141
    /// @notice Returns the json of the deployment transaction given a contract address.
    function _getDeployTransactionByContractAddress(address _addr) internal returns (string memory) {
        string[] memory cmd = new string[](3);
142
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
143
        cmd[1] = "-c";
144 145 146 147 148 149
        cmd[2] = string.concat(
            Executables.jq,
            " -r '.transactions[] | select(.contractAddress == ",
            '"',
            vm.toString(_addr),
            '"',
150 151
            ') | select(.transactionType == "CREATE"',
            ' or .transactionType == "CREATE2"',
152 153 154
            ")' < ",
            deployPath
        );
Mark Tyneway's avatar
Mark Tyneway committed
155
        bytes memory res = vm.ffi(cmd);
156 157 158
        return string(res);
    }

159
    /// @notice Returns the contract name from a deploy transaction.
160
    function _getContractNameFromDeployTransaction(string memory _deployTx) internal pure returns (string memory) {
161 162 163
        return stdJson.readString(_deployTx, ".contractName");
    }

164
    /// @notice Wrapper for vm.getCode that handles semver in the name.
Mark Tyneway's avatar
Mark Tyneway committed
165
    function _getCode(string memory _name) internal returns (bytes memory) {
166 167 168 169 170
        string memory fqn = _getFullyQualifiedName(_name);
        bytes memory code = vm.getCode(fqn);
        return code;
    }

171
    /// @notice Wrapper for vm.getDeployedCode that handles semver in the name.
Mark Tyneway's avatar
Mark Tyneway committed
172
    function _getDeployedCode(string memory _name) internal returns (bytes memory) {
173 174 175 176 177
        string memory fqn = _getFullyQualifiedName(_name);
        bytes memory code = vm.getDeployedCode(fqn);
        return code;
    }

178
    /// @notice Removes the semantic versioning from a contract name. The semver will exist if the contract is compiled
Mark Tyneway's avatar
Mark Tyneway committed
179
    /// more than once with different versions of the compiler.
180
    function _stripSemver(string memory _name) internal returns (string memory) {
181 182 183
        string[] memory cmd = new string[](3);
        cmd[0] = Executables.bash;
        cmd[1] = "-c";
184 185 186
        cmd[2] = string.concat(
            Executables.echo, " ", _name, " | ", Executables.sed, " -E 's/[.][0-9]+\\.[0-9]+\\.[0-9]+//g'"
        );
187
        bytes memory res = vm.ffi(cmd);
Mark Tyneway's avatar
Mark Tyneway committed
188 189 190 191 192 193
        return string(res);
    }

    /// @notice Returns the constructor arguent of a deployment transaction given a transaction json.
    function getDeployTransactionConstructorArguments(string memory _transaction) internal returns (string[] memory) {
        string[] memory cmd = new string[](3);
194
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
195
        cmd[1] = "-c";
196
        cmd[2] = string.concat(Executables.jq, " -r '.arguments' <<< '", _transaction, "'");
Mark Tyneway's avatar
Mark Tyneway committed
197 198 199 200 201 202 203 204 205 206
        bytes memory res = vm.ffi(cmd);

        string[] memory args = new string[](0);
        if (keccak256(bytes("null")) != keccak256(res)) {
            args = stdJson.readStringArray(string(res), "");
        }
        return args;
    }

    /// @notice Builds the fully qualified name of a contract. Assumes that the
207
    ///         file name is the same as the contract name but strips semver for the file name.
208
    function _getFullyQualifiedName(string memory _name) internal returns (string memory) {
209 210
        string memory sanitized = _stripSemver(_name);
        return string.concat(sanitized, ".sol:", _name);
Mark Tyneway's avatar
Mark Tyneway committed
211 212
    }

213
    function _getForgeArtifactDirectory(string memory _name) internal returns (string memory dir_) {
Mark Tyneway's avatar
Mark Tyneway committed
214
        string[] memory cmd = new string[](3);
215
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
216
        cmd[1] = "-c";
Mark Tyneway's avatar
Mark Tyneway committed
217
        cmd[2] = string.concat(Executables.forge, " config --json | ", Executables.jq, " -r .out");
Mark Tyneway's avatar
Mark Tyneway committed
218
        bytes memory res = vm.ffi(cmd);
219
        string memory contractName = _stripSemver(_name);
220
        dir_ = string.concat(vm.projectRoot(), "/", string(res), "/", contractName, ".sol");
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    }

    /// @notice Returns the filesystem path to the artifact path. If the contract was compiled
    ///         with multiple solidity versions then return the first one based on the result of `ls`.
    function _getForgeArtifactPath(string memory _name) internal returns (string memory) {
        string memory directory = _getForgeArtifactDirectory(_name);
        string memory path = string.concat(directory, "/", _name, ".json");
        if (vm.exists(path)) return path;

        string[] memory cmd = new string[](3);
        cmd[0] = Executables.bash;
        cmd[1] = "-c";
        cmd[2] = string.concat(
            Executables.ls,
            " -1 --color=never ",
            directory,
            " | ",
            Executables.jq,
            " -R -s -c 'split(\"\n\") | map(select(length > 0))'"
        );
        bytes memory res = vm.ffi(cmd);
        string[] memory files = stdJson.readStringArray(string(res), "");
        return string.concat(directory, "/", files[0]);
Mark Tyneway's avatar
Mark Tyneway committed
244 245 246 247 248
    }

    /// @notice Returns the forge artifact given a contract name.
    function _getForgeArtifact(string memory _name) internal returns (string memory) {
        string memory forgeArtifactPath = _getForgeArtifactPath(_name);
249
        return vm.readFile(forgeArtifactPath);
Mark Tyneway's avatar
Mark Tyneway committed
250 251 252
    }

    /// @notice Returns the receipt of a deployment transaction.
253
    function _getDeployReceiptByContractAddress(address _addr) internal returns (string memory receipt_) {
Mark Tyneway's avatar
Mark Tyneway committed
254
        string[] memory cmd = new string[](3);
255
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
256
        cmd[1] = "-c";
257 258 259 260
        cmd[2] = string.concat(
            Executables.jq,
            " -r '.receipts[] | select(.contractAddress == ",
            '"',
261
            vm.toString(_addr),
262 263 264 265
            '"',
            ")' < ",
            deployPath
        );
Mark Tyneway's avatar
Mark Tyneway committed
266 267
        bytes memory res = vm.ffi(cmd);
        string memory receipt = string(res);
268
        receipt_ = receipt;
Mark Tyneway's avatar
Mark Tyneway committed
269 270 271
    }

    /// @notice Returns the devdoc for a deployed contract.
272
    function getDevDoc(string memory _name) internal returns (string memory doc_) {
Mark Tyneway's avatar
Mark Tyneway committed
273
        string[] memory cmd = new string[](3);
274
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
275
        cmd[1] = "-c";
276
        cmd[2] = string.concat(Executables.jq, " -r '.devdoc' < ", _getForgeArtifactPath(_name));
Mark Tyneway's avatar
Mark Tyneway committed
277
        bytes memory res = vm.ffi(cmd);
278
        doc_ = string(res);
Mark Tyneway's avatar
Mark Tyneway committed
279 280 281
    }

    /// @notice Returns the storage layout for a deployed contract.
282
    function getStorageLayout(string memory _name) internal returns (string memory layout_) {
Mark Tyneway's avatar
Mark Tyneway committed
283
        string[] memory cmd = new string[](3);
284
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
285
        cmd[1] = "-c";
286
        cmd[2] = string.concat(Executables.jq, " -r '.storageLayout' < ", _getForgeArtifactPath(_name));
Mark Tyneway's avatar
Mark Tyneway committed
287
        bytes memory res = vm.ffi(cmd);
288
        layout_ = string(res);
Mark Tyneway's avatar
Mark Tyneway committed
289 290 291
    }

    /// @notice Returns the abi for a deployed contract.
292
    function getAbi(string memory _name) public returns (string memory abi_) {
Mark Tyneway's avatar
Mark Tyneway committed
293
        string[] memory cmd = new string[](3);
294
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
295
        cmd[1] = "-c";
296
        cmd[2] = string.concat(Executables.jq, " -r '.abi' < ", _getForgeArtifactPath(_name));
Mark Tyneway's avatar
Mark Tyneway committed
297
        bytes memory res = vm.ffi(cmd);
298
        abi_ = string(res);
Mark Tyneway's avatar
Mark Tyneway committed
299 300
    }

301 302 303 304 305 306 307 308 309 310
    /// @notice
    function getMethodIdentifiers(string memory _name) public returns (string[] memory ids_) {
        string[] memory cmd = new string[](3);
        cmd[0] = Executables.bash;
        cmd[1] = "-c";
        cmd[2] = string.concat(Executables.jq, " '.methodIdentifiers | keys' < ", _getForgeArtifactPath(_name));
        bytes memory res = vm.ffi(cmd);
        ids_ = stdJson.readStringArray(string(res), "");
    }

Mark Tyneway's avatar
Mark Tyneway committed
311
    /// @notice Returns the userdoc for a deployed contract.
312
    function getUserDoc(string memory _name) internal returns (string memory doc_) {
Mark Tyneway's avatar
Mark Tyneway committed
313
        string[] memory cmd = new string[](3);
314
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
315
        cmd[1] = "-c";
316
        cmd[2] = string.concat(Executables.jq, " -r '.userdoc' < ", _getForgeArtifactPath(_name));
Mark Tyneway's avatar
Mark Tyneway committed
317
        bytes memory res = vm.ffi(cmd);
318
        doc_ = string(res);
Mark Tyneway's avatar
Mark Tyneway committed
319 320 321
    }

    /// @notice
322
    function getMetadata(string memory _name) internal returns (string memory metadata_) {
Mark Tyneway's avatar
Mark Tyneway committed
323
        string[] memory cmd = new string[](3);
324
        cmd[0] = Executables.bash;
Mark Tyneway's avatar
Mark Tyneway committed
325
        cmd[1] = "-c";
326
        cmd[2] = string.concat(Executables.jq, " '.metadata | tostring' < ", _getForgeArtifactPath(_name));
Mark Tyneway's avatar
Mark Tyneway committed
327
        bytes memory res = vm.ffi(cmd);
328
        metadata_ = string(res);
Mark Tyneway's avatar
Mark Tyneway committed
329 330
    }

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
    /// @dev Pulls the `_initialized` storage slot information from the Forge artifacts for a given contract.
    function getInitializedSlot(string memory _contractName) internal returns (StorageSlot memory slot_) {
        string memory storageLayout = getStorageLayout(_contractName);

        string[] memory command = new string[](3);
        command[0] = Executables.bash;
        command[1] = "-c";
        command[2] = string.concat(
            Executables.echo,
            " '",
            storageLayout,
            "'",
            " | ",
            Executables.jq,
            " '.storage[] | select(.label == \"_initialized\" and .type == \"t_uint8\")'"
        );
        bytes memory rawSlot = vm.parseJson(string(vm.ffi(command)));
        slot_ = abi.decode(rawSlot, (StorageSlot));
    }

clabby's avatar
clabby committed
351
    /// @dev Returns the value of the internal `_initialized` storage slot for a given contract.
352 353 354 355 356 357 358 359 360 361 362 363 364 365
    function loadInitializedSlot(string memory _contractName) public returns (uint8 initialized_) {
        address contractAddress;
        // Check if the contract name ends with `Proxy` and, if so, get the implementation address
        if (LibString.endsWith(_contractName, "Proxy")) {
            contractAddress = EIP1967Helper.getImplementation(getAddress(_contractName));
            _contractName = LibString.slice(_contractName, 0, bytes(_contractName).length - 5);
            // If the EIP1967 implementation address is 0, we try to get the implementation address from legacy
            // AddressManager, which would work if the proxy is ResolvedDelegateProxy like L1CrossDomainMessengerProxy.
            if (contractAddress == address(0)) {
                contractAddress =
                    IAddressManager(mustGetAddress("AddressManager")).getAddress(string.concat("OVM_", _contractName));
            }
        } else {
            contractAddress = mustGetAddress(_contractName);
clabby's avatar
clabby committed
366
        }
367 368
        StorageSlot memory slot = getInitializedSlot(_contractName);
        bytes32 slotVal = vm.load(contractAddress, bytes32(vm.parseUint(slot.slot)));
clabby's avatar
clabby committed
369 370 371
        initialized_ = uint8((uint256(slotVal) >> (slot.offset * 8)) & 0xFF);
    }

Mark Tyneway's avatar
Mark Tyneway committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
    /// @notice Turns an Artifact into a json serialized string
    /// @param _artifact The artifact to serialize
    /// @return The json serialized string
    function _serializeArtifact(Artifact memory _artifact) internal returns (string memory) {
        string memory json = "";
        json = stdJson.serialize("", "address", _artifact.addr);
        json = stdJson.serialize("", "abi", _artifact.abi);
        json = stdJson.serialize("", "args", _artifact.args);
        json = stdJson.serialize("", "bytecode", _artifact.bytecode);
        json = stdJson.serialize("", "deployedBytecode", _artifact.deployedBytecode);
        json = stdJson.serialize("", "devdoc", _artifact.devdoc);
        json = stdJson.serialize("", "metadata", _artifact.metadata);
        json = stdJson.serialize("", "numDeployments", _artifact.numDeployments);
        json = stdJson.serialize("", "receipt", _artifact.receipt);
        json = stdJson.serialize("", "solcInputHash", _artifact.solcInputHash);
        json = stdJson.serialize("", "storageLayout", _artifact.storageLayout);
        json = stdJson.serialize("", "transactionHash", _artifact.transactionHash);
        json = stdJson.serialize("", "userdoc", _artifact.userdoc);
        return json;
    }
}