From dfab5f3114cfa299c0139afbf6b8531b365bc7c8 Mon Sep 17 00:00:00 2001 From: Duc Tho Tran Date: Tue, 10 Oct 2023 11:55:23 +0700 Subject: [PATCH] feat: add script for deployment --- script/BaseDeploy.s.sol | 144 ++++++++++++++++++ script/BaseScript.s.sol | 50 +++++++ script/GeneralConfig.s.sol | 222 ++++++++++++++++++++++++++++ script/LogGenerator.s.sol | 75 ++++++++++ script/interfaces/IDeployScript.sol | 8 + 5 files changed, 499 insertions(+) create mode 100644 script/BaseDeploy.s.sol create mode 100644 script/BaseScript.s.sol create mode 100644 script/GeneralConfig.s.sol create mode 100644 script/LogGenerator.s.sol create mode 100644 script/interfaces/IDeployScript.sol diff --git a/script/BaseDeploy.s.sol b/script/BaseDeploy.s.sol new file mode 100644 index 0000000..f8620ac --- /dev/null +++ b/script/BaseDeploy.s.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ProxyAdmin, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console2} from "forge-std/console2.sol"; +import {StdStyle} from "forge-std/StdStyle.sol"; +import {BaseScript} from "./BaseScript.s.sol"; +import {LogGenerator} from "./LogGenerator.s.sol"; +import "./GeneralConfig.s.sol"; +import {IDeployScript} from "./interfaces/IDeployScript.sol"; + +abstract contract BaseDeploy is BaseScript { + using StdStyle for string; + + bytes public constant EMPTY_ARGS = ""; + + address internal _deployer; + bool internal _alreadySetUp; + bytes internal _overridenArgs; + LogGenerator internal _logger; + mapping(ContractKey contractKey => IDeployScript deployScript) internal _deployScript; + + modifier trySetUp() { + if (!_alreadySetUp) { + setUp(); + _alreadySetUp = true; + } + _; + } + + function setUp() public virtual override { + vm.pauseGasMetering(); + _alreadySetUp = true; + super.setUp(); + + _logger = new LogGenerator(vm, _config); + _setUpDependencies(); + } + + function _setUpDependencies() internal virtual {} + + function _setDependencyDeployScript(ContractKey contractKey, address deployScript) internal { + _deployScript[contractKey] = IDeployScript(deployScript); + } + + function loadContractOrDeploy(ContractKey contractKey) public returns (address payable contractAddr) { + string memory contractName = _config.getContractName(contractKey); + try _config.getAddressFromCurrentNetwork(contractKey) returns (address payable addr) { + contractAddr = addr; + } catch { + console2.log(string.concat("Deployment for ", contractName, " not found, try fresh deploy ...").yellow()); + contractAddr = _deployScript[contractKey].run(); + } + } + + function setArgs(bytes memory args) public returns (IDeployScript) { + _overridenArgs = args; + return IDeployScript(address(this)); + } + + function arguments() public returns (bytes memory args) { + args = _overridenArgs.length == 0 ? _defaultArguments() : _overridenArgs; + } + + function _defaultArguments() internal virtual returns (bytes memory args) {} + + function _deployImmutable(ContractKey contractKey, bytes memory args) internal returns (address payable deployed) { + string memory contractName = _config.getContractName(contractKey); + string memory contractFilename = _config.getContractFileName(contractKey); + uint256 nonce; + (deployed, nonce) = _deployRaw(contractFilename, args); + vm.label(deployed, contractName); + + _config.setAddress(_network, contractKey, deployed); + _logger.generateDeploymentArtifact(_deployer, deployed, contractName, contractName, args, nonce); + } + + function _upgradeProxy(ContractKey contractKey, bytes memory args) internal returns (address payable proxy) { + string memory contractName = _config.getContractName(contractKey); + string memory contractFilename = _config.getContractFileName(contractKey); + + uint256 logicNonce; + address logic; + (logic, logicNonce) = _deployRaw(contractFilename, EMPTY_ARGS); + + ProxyAdmin proxyAdmin = ProxyAdmin(_config.getAddressFromCurrentNetwork(ContractKey.ProxyAdmin)); + proxy = _config.getAddressFromCurrentNetwork(contractKey); + _upgradeRaw(proxyAdmin, ITransparentUpgradeableProxy(proxy), logic, args); + + _logger.generateDeploymentArtifact( + _deployer, logic, contractName, string.concat(contractName, "Logic"), EMPTY_ARGS, logicNonce + ); + } + + function _deployProxy(ContractKey contractKey, bytes memory args) internal returns (address payable deployed) { + string memory contractName = _config.getContractName(contractKey); + string memory contractFilename = _config.getContractFileName(contractKey); + (address logic, uint256 logicNonce) = _deployRaw(contractFilename, EMPTY_ARGS); + + uint256 proxyNonce; + (deployed, proxyNonce) = _deployRaw( + "TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy", + abi.encode(logic, _config.getAddressFromCurrentNetwork(ContractKey.ProxyAdmin), args) + ); + vm.label(deployed, contractName); + + _config.setAddress(_network, contractKey, deployed); + _logger.generateDeploymentArtifact( + _deployer, logic, contractName, string.concat(contractName, "Logic"), EMPTY_ARGS, logicNonce + ); + _logger.generateDeploymentArtifact( + _deployer, deployed, "TransparentUpgradeableProxy", string.concat(contractName, "Proxy"), args, proxyNonce + ); + } + + function _deployRaw(string memory filename, bytes memory args) + internal + returns (address payable deployed, uint256 nonce) + { + nonce = vm.getNonce(_deployer); + address expectedAddr = computeCreateAddress(_deployer, nonce); + + vm.resumeGasMetering(); + vm.broadcast(_deployer); + deployed = payable(deployCode(filename, args)); + vm.pauseGasMetering(); + + require(deployed == expectedAddr, "deployed != expectedAddr"); + } + + function _upgradeRaw(ProxyAdmin proxyAdmin, ITransparentUpgradeableProxy proxy, address logic, bytes memory args) + internal + { + address owner = proxyAdmin.owner(); + + vm.broadcast(owner); + vm.resumeGasMetering(); + if (args.length == 0) proxyAdmin.upgradeAndCall(proxy, logic, ""); + else proxyAdmin.upgradeAndCall(proxy, logic, args); + vm.pauseGasMetering(); + } +} diff --git a/script/BaseScript.s.sol b/script/BaseScript.s.sol new file mode 100644 index 0000000..dc75ace --- /dev/null +++ b/script/BaseScript.s.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Script, console} from "forge-std/Script.sol"; +import {StdAssertions} from "forge-std/StdAssertions.sol"; +import "./GeneralConfig.s.sol"; + +contract BaseScript is Script, StdAssertions { + bytes32 public constant GENERAL_CONFIG_SALT = keccak256(bytes(type(GeneralConfig).name)); + + GeneralConfig internal _config; + Network internal _network; + + modifier onMainnet() { + _network = Network.RoninMainnet; + _; + } + + modifier onTestnet() { + _network = Network.RoninTestnet; + _; + } + + modifier onLocalHost() { + _network = Network.Local; + _; + } + + function setUp() public virtual { + // allow diferrent deploy scripts to share same config storage + // predict general config address + address cfgAddr = computeCreate2Address( + GENERAL_CONFIG_SALT, hashInitCode(abi.encodePacked(type(GeneralConfig).creationCode), abi.encode(vm)) + ); + vm.allowCheatcodes(cfgAddr); + // skip if general config already deployed + if (cfgAddr.code.length == 0) { + vm.prank(CREATE2_FACTORY); + new GeneralConfig{ salt: GENERAL_CONFIG_SALT }(vm); + } + + _config = GeneralConfig(payable(cfgAddr)); + _network = _config.getCurrentNetwork(); + } + + function fail() internal override { + super.fail(); + revert("Got failed assertion"); + } +} diff --git a/script/GeneralConfig.s.sol b/script/GeneralConfig.s.sol new file mode 100644 index 0000000..8a03dda --- /dev/null +++ b/script/GeneralConfig.s.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Vm, VmSafe} from "forge-std/Vm.sol"; +import {LibString} from "solady/src/utils/LibString.sol"; + +enum ContractKey { + ProxyAdmin, + // RNS + RNSRegistry, + RNSRegistrar, + NameWrapper, + NameChecker, + DummyOracle, + RNSAuction, + RNSDomainPrice, + PublicResolver, + ReverseRegistrar, + StablePriceOracle, + RONRegistrarController, + // Land Staking + LandStakingPool, + LandStakingManager +} + +enum Network { + Local, + RoninMainnet, + RoninTestnet +} + +library ChainId { + uint256 public constant LOCAL = 31337; + uint256 public constant RONIN_MAINNET = 2020; + uint256 public constant RONIN_TESTNET = 2021; +} + +contract GeneralConfig { + using LibString for string; + + struct NetworkInfo { + uint256 chainId; + string privateKeyEnvLabel; + string deploymentDir; + } + + string public constant LOCAL_ENV_LABEL = "LOCAL_PK"; + string public constant TESTNET_ENV_LABEL = "TESTNET_PK"; + string public constant MAINNET_ENV_LABEL = "MAINNET_PK"; + string public constant DEPLOYMENT_ROOT = "deployments/"; + string public constant LOCAL_DIR = "local/"; + string public constant RONIN_TESTNET_DIR = "ronin-testnet/"; + string public constant RONIN_MAINNET_DIR = "ronin-mainnet/"; + + Vm private immutable _vm; + + mapping(Network networkIdx => NetworkInfo) private _networkInfoMap; + mapping(uint256 chainId => Network networkIdx) private _networkMap; + mapping(ContractKey contractIdx => string contractName) private _contractNameMap; + mapping(uint256 chainId => mapping(string name => address addr)) private _contractAddrMap; + + constructor(Vm vm) payable { + _vm = vm; + + _setUpNetwork(); + _mapContractKeysToNames(); + + _setUpHardHatDeploymentInfo(); + _setUpFoundryDeploymentInfo(); + + // Manuallly setup for testnet + setAddress(Network.RoninTestnet, ContractKey.LandStakingManager, 0x087c35EEe6b9f697f8CC0062762130E505A39003); + setAddress(Network.RoninTestnet, ContractKey.LandStakingPool, 0x74f4aeB84F1535A777CC9B24f4C4B703F2402868); + + // Manually setup for mainnet + setAddress(Network.RoninMainnet, ContractKey.LandStakingManager, 0x7f27E35170472E7f107d3e55C2B9bCd44aA01dD5); + setAddress(Network.RoninMainnet, ContractKey.LandStakingPool, 0xb2A5110F163eC592F8F0D4207253D8CbC327d9fB); + + // Manually setup for localhost + setAddress(Network.Local, ContractKey.ProxyAdmin, 0x70997970C51812dc3A010C7d01b50e0d17dc79C8); + } + + function _mapContractKeysToNames() internal { + // setup contract name + _contractNameMap[ContractKey.ProxyAdmin] = "ProxyAdmin"; + _contractNameMap[ContractKey.NameChecker] = "NameChecker"; + _contractNameMap[ContractKey.NameWrapper] = "NameWrapper"; + _contractNameMap[ContractKey.RNSAuction] = "RNSAuction"; + _contractNameMap[ContractKey.RNSRegistry] = "RNSRegistry"; + _contractNameMap[ContractKey.RNSRegistrar] = "RNSRegistrar"; + _contractNameMap[ContractKey.DummyOracle] = "MockDummyOracle"; + _contractNameMap[ContractKey.RNSDomainPrice] = "RNSDomainPrice"; + _contractNameMap[ContractKey.PublicResolver] = "PublicResolver"; + _contractNameMap[ContractKey.LandStakingPool] = "LandStakingPool"; + _contractNameMap[ContractKey.ReverseRegistrar] = "ReverseRegistrar"; + _contractNameMap[ContractKey.LandStakingManager] = "LandStakingManager"; + _contractNameMap[ContractKey.StablePriceOracle] = "MockStablePriceOracle"; + _contractNameMap[ContractKey.RONRegistrarController] = "RONRegistrarController"; + } + + function _setUpNetwork() internal { + _networkMap[ChainId.LOCAL] = Network.Local; + _networkInfoMap[Network.Local] = NetworkInfo(ChainId.LOCAL, LOCAL_ENV_LABEL, LOCAL_DIR); + + _networkMap[ChainId.RONIN_TESTNET] = Network.RoninTestnet; + _networkInfoMap[Network.RoninTestnet] = NetworkInfo(ChainId.RONIN_TESTNET, TESTNET_ENV_LABEL, RONIN_TESTNET_DIR); + + _networkMap[ChainId.RONIN_MAINNET] = Network.RoninMainnet; + _networkInfoMap[Network.RoninMainnet] = NetworkInfo(ChainId.RONIN_MAINNET, MAINNET_ENV_LABEL, RONIN_MAINNET_DIR); + } + + function _setUpFoundryDeploymentInfo() internal {} + + function _setUpHardHatDeploymentInfo() internal { + VmSafe.DirEntry[] memory deployments = _vm.readDir(DEPLOYMENT_ROOT); + + for (uint256 i; i < deployments.length;) { + VmSafe.DirEntry[] memory entries = _vm.readDir(deployments[i].path); + uint256 chainId = _vm.parseUint(_vm.readFile(string.concat(deployments[i].path, "/.chainId"))); + string[] memory s = deployments[i].path.split("/"); + string memory prefix = s[s.length - 1]; + + for (uint256 j; j < entries.length;) { + string memory path = entries[j].path; + + if (path.endsWith(".json")) { + // filter out logic deployments + if (!path.endsWith("Logic.json")) { + string[] memory splitteds = path.split("/"); + string memory contractName = splitteds[splitteds.length - 1]; + string memory suffix = path.endsWith("Proxy.json") ? "Proxy.json" : ".json"; + // remove suffix + assembly ("memory-safe") { + mstore(contractName, sub(mload(contractName), mload(suffix))) + } + string memory json = _vm.readFile(path); + address contractAddr = _vm.parseJsonAddress(json, ".address"); + + _vm.label(contractAddr, string.concat(prefix, ".", contractName)); + _contractAddrMap[chainId][contractName] = contractAddr; + } + } + + unchecked { + ++j; + } + } + + unchecked { + ++i; + } + } + } + + function setAddressForCurrentNetwork(ContractKey contractKey, address contractAddr) public { + setAddress(getCurrentNetwork(), contractKey, contractAddr); + } + + function setAddress(Network network, ContractKey contractKey, address contractAddr) public { + uint256 chainId = _networkInfoMap[network].chainId; + string memory contractName = _contractNameMap[contractKey]; + require(chainId != 0 && bytes(contractName).length != 0, "Network or Contract Key not found"); + + _contractAddrMap[chainId][contractName] = contractAddr; + } + + function getDeploymentDirectoryFromCurrentNetwork() public view returns (string memory dirPath) { + dirPath = getDeploymentDirectory(getCurrentNetwork()); + } + + function getDeploymentDirectory(Network network) public view returns (string memory dirPath) { + string memory dirName = _networkInfoMap[network].deploymentDir; + require(bytes(dirName).length != 0, "Deployment dir not found"); + dirPath = string.concat(DEPLOYMENT_ROOT, dirName); + } + + function getPrivateKeyEnvLabelFromCurrentNetwork() public view returns (string memory privatekeyEnvLabel) { + privatekeyEnvLabel = getPrivateKeyEnvLabel(getCurrentNetwork()); + } + + function getPrivateKeyEnvLabel(Network network) public view returns (string memory privateKeyEnvLabel) { + privateKeyEnvLabel = _networkInfoMap[network].privateKeyEnvLabel; + require(bytes(privateKeyEnvLabel).length != 0, "ENV label not found"); + } + + function getContractName(ContractKey contractKey) public view returns (string memory name) { + name = _contractNameMap[contractKey]; + require(bytes(name).length != 0, "Contract Key not found"); + } + + function getContractFileName(ContractKey contractKey) public view returns (string memory filename) { + string memory contractName = getContractName(contractKey); + filename = string.concat(contractName, ".sol:", contractName); + } + + function getCurrentNetwork() public view returns (Network network) { + network = _networkMap[block.chainid]; + } + + function getNetworkByChainId(uint256 chainId) public view returns (Network network) { + network = _networkMap[chainId]; + } + + function getAddress(Network network, ContractKey contractKey) public view returns (address payable) { + return getAddressByRawData(_networkInfoMap[network].chainId, _contractNameMap[contractKey]); + } + + function getAddressFromCurrentNetwork(ContractKey contractKey) public view returns (address payable) { + string memory contractName = _contractNameMap[contractKey]; + require(bytes(contractName).length != 0, "Contract Key not found"); + return getAddressByRawData(block.chainid, contractName); + } + + function getAddressByString(string memory contractName) public view returns (address payable) { + return getAddressByRawData(block.chainid, contractName); + } + + function getAddressByRawData(uint256 chainId, string memory contractName) public view returns (address payable addr) { + addr = payable(_contractAddrMap[chainId][contractName]); + require(addr != address(0), string.concat("address not found: ", contractName)); + } +} diff --git a/script/LogGenerator.s.sol b/script/LogGenerator.s.sol new file mode 100644 index 0000000..badd8ae --- /dev/null +++ b/script/LogGenerator.s.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Vm} from "forge-std/Vm.sol"; +import {StdStyle} from "forge-std/StdStyle.sol"; +import {console2} from "forge-std/console2.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {GeneralConfig} from "./GeneralConfig.s.sol"; +import {JSONParserLib} from "solady/src/utils/JSONParserLib.sol"; + +contract LogGenerator { + using StdStyle for *; + using stdJson for string; + using JSONParserLib for *; + + Vm private immutable _vm; + GeneralConfig private immutable _config; + + constructor(Vm vm, GeneralConfig config) { + _vm = vm; + _config = config; + } + + function generateDeploymentArtifact( + address deployer, + address contractAddr, + string memory contractName, + string memory fileName, + bytes memory args, + uint256 nonce + ) external { + // skip writing artifact if network is localhost + // if (_network == Network.LocalHost) return; + string memory dirPath = _config.getDeploymentDirectory(_config.getCurrentNetwork()); + string memory filePath = string.concat(dirPath, fileName, ".json"); + + string memory json; + uint256 numDeployments = 1; + + if (_vm.exists(filePath)) { + string memory existedJson = _vm.readFile(filePath); + if (_vm.keyExists(existedJson, ".numDeployments")) { + numDeployments = _vm.parseJsonUint(_vm.readFile(filePath), ".numDeployments"); + numDeployments += 1; + } + } + + json.serialize("nonce", nonce); + json.serialize("args", args); + json.serialize("chainId", block.chainid); + json.serialize("deployer", deployer); + json.serialize("address", contractAddr); + json.serialize("timestamp", block.timestamp); + json.serialize("contractName", contractName); + json.serialize("numDeployments", numDeployments); + json.serialize("blockNumber", block.number); + json.serialize("isFoundry", true); + + string memory artifactPath = string.concat("./out/", contractName, ".sol/", contractName, ".json"); + string memory artifact = _vm.readFile(artifactPath); + JSONParserLib.Item memory item = artifact.parse(); + + json.serialize("bytecode", item.at('"bytecode"').at('"object"').value()); + json.serialize("deployedBytecode", item.at('"deployedBytecode"').at('"object"').value()); + json.serialize("storageLayout", item.at('"storageLayout"').value()); + json.serialize("userdoc", item.at('"userdoc"').value()); + json.serialize("devdoc", item.at('"devdoc"').value()); + json.serialize("abi", item.at('"abi"').value()); + json = json.serialize("metadata", item.at('"rawMetadata"').value()); + + json.write(filePath); + + console2.log(string.concat(fileName, " deployed at:").green(), contractAddr.green()); + } +} diff --git a/script/interfaces/IDeployScript.sol b/script/interfaces/IDeployScript.sol new file mode 100644 index 0000000..d5b010e --- /dev/null +++ b/script/interfaces/IDeployScript.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IDeployScript { + function run() external returns (address payable); + + function setArgs(bytes calldata args) external returns (IDeployScript); +}