packages/contracts-core/script/utils/DeployerUtils.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {console, Script, stdJson} from "forge-std/Script.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {CREATE3Factory} from "create3/CREATE3Factory.sol";
interface ICreate3Factory {
function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address deployed);
function getDeployed(address deployer, bytes32 salt) external view returns (address deployed);
}
// solhint-disable no-console
// solhint-disable no-empty-blocks
// solhint-disable ordering
contract DeployerUtils is Script {
using stdJson for string;
/// @dev Path to artifacts, deployments and configs directories
string private constant ARTIFACTS = "artifacts/";
string private constant DEPLOYMENTS = "deployments/";
string private deployConfigs = "script/configs/";
// @dev wether or not devnet is enabled
bool private devnetEnabled = false;
// @dev env var for wether or not devnet is in use
string private constant DEVNET_ENABLED_VAR = "DEVNET";
// @dev artifacted path to use if devnet is enabled;
string private constant DEPLOY_CONFIGS_DEVNET = "script/configs/devnet/";
/// @dev Whether the script will be broadcasted or not
bool internal isBroadcasted = false;
/// @dev Current chain alias
string internal chainAlias;
/// @dev Private key and address for deploying contracts
uint256 internal broadcasterPK;
address internal broadcasterAddress;
ICreate3Factory private factory = ICreate3Factory(0x9fBB3DF7C40Da2e5A0dE984fFE2CCB7C47cd0ABf);
bytes32 internal deploymentSalt;
/// @notice Prevents this contract from being included in the coverage report
function testDeployerUtils() external {}
// ═══════════════════════════════════════════════════ SETUP ═══════════════════════════════════════════════════════
function stopBroadcast() public {
vm.stopBroadcast();
isBroadcasted = false;
}
function startBroadcast(bool isBroadcasted_) public {
chainAlias = getChainAlias();
if (isBroadcasted_) createDir(string.concat(DEPLOYMENTS, chainAlias));
vm.startBroadcast(broadcasterPK);
isBroadcasted = isBroadcasted_;
}
// @dev this is called just in time so we can make sure that startBroadcast() is called before this.
// it's only overriden and deployed just-in-time in the case of devnet
// TODO: this pattern sucks. It introduces potential unexpected behavior if the dev calls factory. directly.
// It's also slow.
function getFactory() internal returns (ICreate3Factory) {
if (!devnetEnabled) {
return factory;
}
address factoryDeployment = tryLoadDeployment("CREATE3Factory");
if (factoryDeployment == address(0)) {
if (broadcasterPK == 0) {
console.log("please setup a private key before calling this function");
}
console.log("Create3Factory not deployed on devnet, deploying now");
CREATE3Factory NewFactory = new CREATE3Factory();
saveDeployment("Create3Factory", "Create3Factory", address(NewFactory), "0x");
factoryDeployment = address(NewFactory);
}
factory = ICreate3Factory(factoryDeployment);
return factory;
}
// @dev must be called after setupPK()
function setupDevnetIfEnabled() internal {
devnetEnabled = vm.envOr(DEVNET_ENABLED_VAR, false);
if (devnetEnabled) {
devnetEnabled = true;
// setup the chains
setChain("chain_a", Chain("chain_a", 42, "chain_a", "http://localhost:9001/rpc/42"));
setChain("chain_b", Chain("chain_b", 43, "chain_b", "http://localhost:9001/rpc/43"));
setChain("chain_c", Chain("chain_c", 44, "chain_c", "http://localhost:9001/rpc/44"));
// override the configs path
deployConfigs = DEPLOY_CONFIGS_DEVNET;
chainAlias = getChainAlias();
}
}
function setupDeployerPK() public {
setupPK("DEPLOYER_PRIVATE_KEY");
}
function setupPK(string memory pkEnvKey) public {
// Load deployer PK from .env
broadcasterPK = vm.envOr(pkEnvKey, uint256(0));
if (broadcasterPK == 0) {
console.log("Key not specified in .env: %s", pkEnvKey);
} else {
// Derive deployer address
broadcasterAddress = vm.addr(broadcasterPK);
console.log("Deployer address: %s", broadcasterAddress);
console.log("Deployer balance: %s", _fromWei(broadcasterAddress.balance));
}
}
/// @notice Returns name of the current chain.
function getChainAlias() public returns (string memory) {
return getChain(block.chainid).chainAlias;
}
// ════════════════════════════════════════════════ DEPLOYMENTS ════════════════════════════════════════════════════
/// @notice Deploys the contract using Create3Factory. Does not save anything.
function factoryDeploy(string memory contractName, bytes memory creationCode, bytes memory constructorArgs)
internal
returns (address deployment)
{
require(Address.isContract(address(getFactory())), "Factory not deployed");
deployment = getFactory().deploy(
getDeploymentSalt(contractName), // salt
abi.encodePacked(creationCode, constructorArgs) // creation code with appended constructor args
);
require(deployment != address(0), "Factory deployment failed");
}
/// @notice Gets the deployment salt for a given contract.
function getDeploymentSalt(string memory contractName) internal view returns (bytes32) {
return keccak256(bytes.concat(deploymentSalt, bytes(contractName)));
}
/// @notice Predicts the deployment address for a contract.
function predictFactoryDeployment(string memory contractName) internal returns (address) {
ICreate3Factory _factory = getFactory();
require(Address.isContract(address(_factory)), "Factory not deployed");
return _factory.getDeployed(broadcasterAddress, getDeploymentSalt(contractName));
}
/// @notice Deploys the contract and saves the deployment artifact
/// @dev Will reuse existing deployment, if it exists
/// @param contractName Contract name to deploy
/// @param deployFunc Callback function to deploy a requested contract
/// @return deployment The deployment address
function deployContract(
string memory contractName,
function() internal returns (address, bytes memory) deployFunc,
function(address) internal initFunc
) internal returns (address deployment, bytes memory constructorArgs) {
return deployContract(contractName, contractName, deployFunc, initFunc);
}
/// @notice Deploys the contract and saves the deployment artifact under specified name
/// @dev Will reuse existing deployment, if it exists
/// @param contractName Contract name to deploy
/// @param saveAsName Name to use for saving the deployment artifact
/// @param deployFunc Callback function to deploy a requested contract
/// @param initFunc Callback function to initialize a deployed contract
/// @return deployment The deployment address
function deployContract(
string memory contractName,
string memory saveAsName,
function() internal returns (address, bytes memory) deployFunc,
function(address) internal initFunc
) internal returns (address deployment, bytes memory constructorArgs) {
deployment = tryLoadDeployment(saveAsName);
if (deployment == address(0)) {
(deployment, constructorArgs) = deployFunc();
saveDeployment(contractName, saveAsName, deployment, constructorArgs);
} else {
console.log("Reusing existing deployment for %s: %s", contractName, deployment);
}
vm.label(deployment, contractName);
initFunc(deployment);
}
/// @notice Returns the deployment for a contract on the current chain, if it exists.
/// Reverts if it doesn't exist.
function loadDeployment(string memory contractName) public returns (address deployment) {
deployment = tryLoadDeployment(contractName);
require(deployment != address(0), string.concat(contractName, " doesn't exist on ", chainAlias));
}
/// @notice Returns the deployment for a contract on the current chain, if it exists.
/// Returns address(0), if it doesn't exist.
function tryLoadDeployment(string memory contractName) public returns (address deployment) {
try vm.readFile(deploymentPath(contractName)) returns (string memory json) {
// We assume that if a deployment file exists, the contract is indeed deployed
deployment = json.readAddress(".address");
} catch {
// Doesn't exist
deployment = address(0);
}
}
/// @notice Saves the deployment JSON for a deployed contract.
function saveDeployment(
string memory contractName,
string memory saveAsName,
address deployedAt,
bytes memory constructorArgs
) public {
console.log("Deployed: [%s] on [%s] at %s", contractName, chainAlias, deployedAt);
// Do nothing if script isn't broadcasted
if (!isBroadcasted) return;
// Otherwise, save the deployment JSON
string memory deployment = "deployment";
// First, write only the deployment address and the constructor args
deployment.serialize("address", deployedAt);
deployment = deployment.serialize("args", constructorArgs);
deployment.write(deploymentPath(saveAsName));
// Then, initiate the jq command to add "abi" as the next key
// This makes sure that "address" value is printed first later
string[] memory inputs = new string[](6);
inputs[0] = "jq";
// Read the full artifact file into $artifact variable
inputs[1] = "--argfile";
inputs[2] = "artifact";
inputs[3] = artifactPath(contractName);
// Set value for ".abi" key to artifact's ABI
inputs[4] = ".abi = $artifact.abi";
inputs[5] = deploymentPath(saveAsName);
bytes memory full = vm.ffi(inputs);
// Finally, print the updated deployment JSON
string(full).write(deploymentPath(saveAsName));
}
// ═══════════════════════════════════════════════ DEPLOY CONFIG ═══════════════════════════════════════════════════
/// @notice Checks if deploy config exists for a given contract on the current chain.
function deployConfigExists(string memory contractName) public returns (bool) {
try vm.fsMetadata(deployConfigPath(contractName)) {
return true;
} catch {
return false;
}
}
/// @notice Loads deploy config for a given contract on the current chain.
/// Will revert if config doesn't exist.
function loadDeployConfig(string memory contractName) public returns (string memory json) {
return vm.readFile(deployConfigPath(contractName));
}
/// @notice Saves deploy config for a given contract on the current chain.
function saveDeployConfig(string memory contractName, string memory config) public {
console.log("Saved: config for [%s] on [%s]", contractName, chainAlias);
string memory path = deployConfigPath(contractName);
vm.writeJson(config, path);
// Sort keys in config JSON for consistency
sortJSON(path);
}
/// @notice Loads deploy config for a given contract on the current chain.
/// Will revert if config doesn't exist.
function loadGlobalDeployConfig(string memory contractName) public returns (string memory json) {
return vm.readFile(globalDeployConfigPath(contractName));
}
// ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════
/// @notice Returns path to the contract artifact.
function artifactPath(string memory contractName) public pure returns (string memory path) {
return string.concat(ARTIFACTS, contractName, ".sol/", deploymentFn(contractName));
}
/// @notice Returns deployment filename for the contract.
function deploymentFn(string memory contractName) public pure returns (string memory path) {
return string.concat(contractName, ".json");
}
/// @notice Returns path to the contract deployment for the current chain.
function deploymentPath(string memory contractName) public view returns (string memory path) {
require(bytes(chainAlias).length != 0, "Chain not set");
return string.concat(DEPLOYMENTS, chainAlias, "/", deploymentFn(contractName));
}
/// @notice Returns deploy config filename for the contract.
function deployConfigFn(string memory contractName) public pure returns (string memory path) {
return string.concat(contractName, ".dc.json");
}
/// @notice Returns path to the contract deploy config JSON on the current chain.
function deployConfigPath(string memory contractName) internal returns (string memory path) {
require(bytes(chainAlias).length != 0, "Chain not set");
return string.concat(deployConfigs, chainAlias, "/", deployConfigFn(contractName));
}
/// @notice Returns path to the global contract deploy config JSON.
function globalDeployConfigPath(string memory contractName) public returns (string memory path) {
return string.concat(deployConfigs, deployConfigFn(contractName));
}
/// @notice Create directory if it not exists already
function createDir(string memory dirPath) public {
// solhint-disable-next-line no-empty-blocks
try vm.fsMetadata(dirPath) {}
catch {
string[] memory inputs = new string[](3);
inputs[0] = "mkdir";
inputs[1] = "--p";
inputs[2] = dirPath;
vm.ffi(inputs);
}
}
/// @dev Reads JSON from given path, sorts its keys and overwrites the file.
function sortJSON(string memory path) public {
string[] memory inputs = new string[](4);
inputs[0] = "jq";
// sort keys of objects on output
inputs[1] = "-S";
// The simplest filter is ., which copies jq's input to its output unmodified
inputs[2] = ".";
inputs[3] = path;
bytes memory sorted = vm.ffi(inputs);
string(sorted).write(path);
}
// ═════════════════════════════════════════════ INTERNAL HELPERS ══════════════════════════════════════════════════
function _fromWei(uint256 amount) internal pure returns (string memory s) {
string memory a = Strings.toString(amount / 10 ** 18);
string memory b = Strings.toString(amount % 10 ** 18);
// Add leading zeroes to the decimal part
while (bytes(b).length < 18) {
b = string.concat("0", b);
}
return string.concat(a, ".", b);
}
}