From 9ac8bc66e4319c79fe0ef12b817393a9d88f2950 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Wed, 4 Sep 2024 14:30:07 +0700 Subject: [PATCH 1/5] feat: simulation support in deploy-cli --- contracts/.gitignore | 1 + contracts/foundry.toml | 5 +- contracts/src/scripts/DeployLiquity2.s.sol | 116 +++++++++++----- contracts/utils/deploy-cli.ts | 129 +++++------------- .../utils/deployment-artifacts-to-app-env.ts | 1 - 5 files changed, 127 insertions(+), 125 deletions(-) diff --git a/contracts/.gitignore b/contracts/.gitignore index dec881e9..7588d821 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -28,3 +28,4 @@ coverage.json mochaOutput.json testMatrix.json /deployment-context-latest.json +/deployment-manifest.json diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 828845de..7ae2b66b 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -4,7 +4,10 @@ out = "out" libs = ["lib"] evm_version = 'shanghai' ignored_error_codes = [5574] # contract-size -fs_permissions = [{access = "read", path = "./utils/assets/"}] +fs_permissions = [ + { access = "read", path = "./utils/assets/" }, + { access = "write", path = "./deployment-manifest.json" } +] [invariant] call_override = false diff --git a/contracts/src/scripts/DeployLiquity2.s.sol b/contracts/src/scripts/DeployLiquity2.s.sol index e7a68664..4e3439b6 100644 --- a/contracts/src/scripts/DeployLiquity2.s.sol +++ b/contracts/src/scripts/DeployLiquity2.s.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.18; import {Script} from "forge-std/Script.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import {StringFormatting} from "../test/Utils/StringFormatting.sol"; import {Accounts} from "../test/TestContracts/Accounts.sol"; import {ERC20Faucet} from "../test/TestContracts/ERC20Faucet.sol"; import {ETH_GAS_COMPENSATION} from "../Dependencies/Constants.sol"; @@ -31,8 +33,10 @@ import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import "forge-std/console.sol"; contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { - bytes32 SALT; + using Strings for *; + using StringFormatting for *; + bytes32 SALT; address deployer; struct LiquityContractsTestnet { @@ -45,6 +49,7 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { IStabilityPool stabilityPool; ITroveManager troveManager; ITroveNFT troveNFT; + MetadataNFT metadataNFT; IPriceFeedTestnet priceFeed; // Tester GasPool gasPool; IInterestRouter interestRouter; @@ -94,6 +99,63 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { uint256 annualInterestRate; } + struct DeploymentResult { + LiquityContractsTestnet[] contractsArray; + ICollateralRegistry collateralRegistry; + IBoldToken boldToken; + HintHelpers hintHelpers; + MultiTroveGetter multiTroveGetter; + } + + function _getBranchContractsJson(LiquityContractsTestnet memory c) internal pure returns (string memory) { + return string.concat( + "{", + string.concat( + // Avoid stack too deep by chunking concats + string.concat( + string.concat('"addressesRegistry":"', address(c.addressesRegistry).toHexString(), '",'), + string.concat('"activePool":"', address(c.activePool).toHexString(), '",'), + string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), + string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), + string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), + string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",'), + string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), + string.concat('"troveManager":"', address(c.troveManager).toHexString(), '",') + ), + string.concat( + string.concat('"troveNFT":"', address(c.troveNFT).toHexString(), '",'), + string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), + string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), + string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), + string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",'), + string.concat('"collToken":"', address(c.collToken).toHexString(), '"') // no comma + ) + ), + "}" + ); + } + + function _getManifestJson(DeploymentResult memory deployed) internal pure returns (string memory) { + string[] memory branches = new string[](deployed.contractsArray.length); + + // Poor man's .map() + for (uint256 i = 0; i < branches.length; ++i) { + branches[i] = _getBranchContractsJson(deployed.contractsArray[i]); + } + + return string.concat( + "{", + string.concat( + string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), + string.concat('"boldToken":"', address(deployed.boldToken).toHexString(), '",'), + string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), + string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), + string.concat('"branches":[', branches.join(","), "]") // no comma + ), + "}" + ); + } + function run() external { SALT = keccak256(abi.encodePacked(block.timestamp)); @@ -114,14 +176,12 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { troveManagerParamsArray[1] = TroveManagerParams(150e16, 120e16, 110e16, 5e16, 10e16); // stETH // used for gas compensation and as collateral of the first branch - IWETH WETH = new WETHTester( - 100 ether, // _tapAmount - 1 days // _tapPeriod - ); - (LiquityContractsTestnet[] memory contractsArray,,,,) = - _deployAndConnectContracts(troveManagerParamsArray, WETH); + IWETH WETH = new WETHTester({_tapAmount: 100 ether, _tapPeriod: 1 days}); + DeploymentResult memory deployed = _deployAndConnectContracts(troveManagerParamsArray, WETH); vm.stopBroadcast(); + vm.writeFile("deployment-manifest.json", _getManifestJson(deployed)); + if (vm.envOr("OPEN_DEMO_TROVES", false)) { // Anvil default accounts // TODO: get accounts from env @@ -157,11 +217,11 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { demoTroves[14] = DemoTroveParams(1, demoAccounts[6], 1, 71e18, 11000e18, 3.3e16); demoTroves[15] = DemoTroveParams(1, demoAccounts[7], 1, 84e18, 12800e18, 4.4e16); - for (uint256 i = 0; i < contractsArray.length; i++) { - tapFaucet(demoAccounts, contractsArray[i]); + for (uint256 i = 0; i < deployed.contractsArray.length; i++) { + tapFaucet(demoAccounts, deployed.contractsArray[i]); } - openDemoTroves(demoTroves, contractsArray); + openDemoTroves(demoTroves, deployed.contractsArray); } } @@ -227,23 +287,17 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { function _deployAndConnectContracts(TroveManagerParams[] memory troveManagerParamsArray, IWETH _WETH) internal - returns ( - LiquityContractsTestnet[] memory contractsArray, - ICollateralRegistry collateralRegistry, - IBoldToken boldToken, - HintHelpers hintHelpers, - MultiTroveGetter multiTroveGetter - ) + returns (DeploymentResult memory r) { DeploymentVarsTestnet memory vars; vars.numCollaterals = troveManagerParamsArray.length; // Deploy Bold vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(deployer)); vars.boldTokenAddress = vm.computeCreate2Address(SALT, keccak256(vars.bytecode)); - boldToken = new BoldToken{salt: SALT}(deployer); - assert(address(boldToken) == vars.boldTokenAddress); + r.boldToken = new BoldToken{salt: SALT}(deployer); + assert(address(r.boldToken) == vars.boldTokenAddress); - contractsArray = new LiquityContractsTestnet[](vars.numCollaterals); + r.contractsArray = new LiquityContractsTestnet[](vars.numCollaterals); vars.collaterals = new IERC20Metadata[](vars.numCollaterals); vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); @@ -269,26 +323,26 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } - collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers); - hintHelpers = new HintHelpers(collateralRegistry); - multiTroveGetter = new MultiTroveGetter(collateralRegistry); + r.collateralRegistry = new CollateralRegistry(r.boldToken, vars.collaterals, vars.troveManagers); + r.hintHelpers = new HintHelpers(r.collateralRegistry); + r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); // Deploy per-branch contracts for each branch for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { vars.contracts = _deployAndConnectCollateralContractsTestnet( vars.collaterals[vars.i], - boldToken, - collateralRegistry, + r.boldToken, + r.collateralRegistry, _WETH, vars.addressesRegistries[vars.i], address(vars.troveManagers[vars.i]), - hintHelpers, - multiTroveGetter + r.hintHelpers, + r.multiTroveGetter ); - contractsArray[vars.i] = vars.contracts; + r.contractsArray[vars.i] = vars.contracts; } - boldToken.setCollateralRegistry(address(collateralRegistry)); + r.boldToken.setCollateralRegistry(address(r.collateralRegistry)); } function _deployAddressesRegistry(TroveManagerParams memory _troveManagerParams) @@ -327,11 +381,11 @@ contract DeployLiquity2Script is Script, StdCheats, MetadataDeployment { contracts.addressesRegistry = _addressesRegistry; // Deploy Metadata - MetadataNFT metadataNFT = deployMetadata(SALT); + contracts.metadataNFT = deployMetadata(SALT); addresses.metadataNFT = vm.computeCreate2Address( SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) ); - assert(address(metadataNFT) == addresses.metadataNFT); + assert(address(contracts.metadataNFT) == addresses.metadataNFT); contracts.priceFeed = new PriceFeedTestnet(); contracts.interestRouter = new MockInterestRouter(); diff --git a/contracts/utils/deploy-cli.ts b/contracts/utils/deploy-cli.ts index 78fa6125..261f1099 100644 --- a/contracts/utils/deploy-cli.ts +++ b/contracts/utils/deploy-cli.ts @@ -25,9 +25,13 @@ Options: --etherscan-api-key Etherscan API key to verify the contracts (required when verifying with Etherscan). --help, -h Show this help message. + --dry-run Don't broadcast transaction, only + simulate execution. --open-demo-troves Open demo troves after deployment (local only). --rpc-url RPC URL to use. + --slow Only send a transaction after the previous + one has been confirmed. --verify Verify contracts after deployment. --verifier Verification provider to use. Possible values: etherscan, sourcify. @@ -57,6 +61,8 @@ const argv = minimist(process.argv.slice(2), { "help", "open-demo-troves", "verify", + "dry-run", + "slow", ], string: [ "chain-id", @@ -122,9 +128,16 @@ export async function main() { String(options.chainId), "--rpc-url", options.rpcUrl, - "--broadcast", ]; + if (!options.dryRun) { + forgeArgs.push("--broadcast"); + } + + if (options.slow) { + forgeArgs.push("--slow"); + } + // verify if (options.verify) { forgeArgs.push("--verify"); @@ -189,31 +202,41 @@ Deploying Liquity contracts with the following settings: // deploy await $`forge ${forgeArgs}`; - const deployedContracts = await getDeployedContracts( - `broadcast/DeployLiquity2.s.sol/${options.chainId}/run-latest.json`, - ); - - const collateralContracts = await getAllCollateralsContracts(deployedContracts, options); + const deploymentManifestJson = fs.readFileSync("deployment-manifest.json", "utf-8"); + const deploymentManifest = JSON.parse(deploymentManifestJson); // XXX hotfix: we were leaking Github secrets in "deployer" // TODO: check if "deployer" is a private key, and calculate its address and use it instead? const { deployer, ...safeOptions } = options; - const protocolContracts = Object.fromEntries( - filterProtocolContracts(deployedContracts), - ); + const protocolContracts = { + WETHTester: deploymentManifest.branches[0].collToken as string, + BoldToken: deploymentManifest.boldToken as string, + CollateralRegistry: deploymentManifest.collateralRegistry as string, + HintHelpers: deploymentManifest.hintHelpers as string, + MultiTroveGetter: deploymentManifest.multiTroveGetter as string, + }; + + const collateralContracts = (deploymentManifest.branches as any[]).map((branch) => ({ + activePool: branch.activePool as string, + borrowerOperations: branch.borrowerOperations as string, + sortedTroves: branch.sortedTroves as string, + stabilityPool: branch.stabilityPool as string, + token: branch.collToken as string, + troveManager: branch.troveManager as string, + })); // write env file await fs.writeJson("deployment-context-latest.json", { options: safeOptions, - deployedContracts, collateralContracts, protocolContracts, }); // format deployed contracts const longestContractName = Math.max( - ...deployedContracts.map(([name]) => name.length), + ...Object.keys(protocolContracts).map((name) => name.length), + ...collateralContracts.flatMap((contracts) => Object.keys(contracts).map((name) => name.length)), ); const formatContracts = (contracts: Array) => @@ -221,7 +244,7 @@ Deploying Liquity contracts with the following settings: echo("Protocol contracts:"); echo(""); - echo(formatContracts(filterProtocolContracts(deployedContracts))); + echo(formatContracts(Object.entries(protocolContracts))); echo(""); echo( collateralContracts.map((collateral, index) => ( @@ -279,10 +302,6 @@ async function getDeployedContracts(jsonPath: string) { throw new Error("Invalid deployment log: " + JSON.stringify(latestRun)); } -function filterProtocolContracts(contracts: Awaited>) { - return contracts.filter(([name]) => PROTOCOL_CONTRACTS_VALID_NAMES.includes(name)); -} - function safeParseInt(value: string) { const parsed = parseInt(value, 10); return isNaN(parsed) ? undefined : parsed; @@ -298,6 +317,8 @@ async function parseArgs() { ledgerPath: argv["ledger-path"], openDemoTroves: argv["open-demo-troves"], rpcUrl: argv["rpc-url"], + dryRun: argv["dry-run"], + slow: argv["slow"], verify: argv["verify"], verifier: argv["verifier"], verifierUrl: argv["verifier-url"], @@ -324,79 +345,3 @@ async function parseArgs() { return { options, networkPreset }; } - -async function castCall( - rpcUrl: string, - contract: string, - method: string, - ...args: string[] -) { - try { - const result = await $`cast call ${contract} ${method} ${args.join(" ")} --rpc-url '${rpcUrl}'`; - return result.stdout.trim(); - } catch (error) { - console.error(`Error calling ${contract} ${method} ${args.join(" ")}: ${error}`); - throw error; - } -} - -async function getCollateralContracts( - collateralIndex: number, - collateralRegistry: string, - rpcUrl: string, -) { - const [token, troveManager] = await Promise.all([ - castCall(rpcUrl, collateralRegistry, "getToken(uint256)(address)", String(collateralIndex)), - castCall(rpcUrl, collateralRegistry, "getTroveManager(uint256)(address)", String(collateralIndex)), - ]); - - const [ - activePool, - borrowerOperations, - sortedTroves, - stabilityPool, - ] = await Promise.all([ - castCall(rpcUrl, troveManager, "activePool()(address)"), - castCall(rpcUrl, troveManager, "borrowerOperations()(address)"), - castCall(rpcUrl, troveManager, "sortedTroves()(address)"), - castCall(rpcUrl, troveManager, "stabilityPool()(address)"), - ]); - - return { - activePool, - borrowerOperations, - sortedTroves, - stabilityPool, - token, - troveManager, - }; -} - -async function getAllCollateralsContracts( - deployedContracts: Array, - options: Awaited>["options"], -) { - const deployedContractsRecord = Object.fromEntries(deployedContracts); - - const ccall = async (contract: string, method: string, ...args: string[]) => { - const result = await $`cast call ${contract} ${method} ${args.join(" ")} --rpc-url '${options.rpcUrl}'`; - return result.stdout.trim(); - }; - - const totalCollaterals = Number( - await ccall( - deployedContractsRecord.CollateralRegistry, - "totalCollaterals()", - ), - ); - - return Promise.all( - Array.from({ length: totalCollaterals }, (_, index) => ( - getCollateralContracts( - index, - deployedContractsRecord.CollateralRegistry, - options.rpcUrl, - ) - )), - ); -} diff --git a/contracts/utils/deployment-artifacts-to-app-env.ts b/contracts/utils/deployment-artifacts-to-app-env.ts index e11cf6d4..c51cf500 100644 --- a/contracts/utils/deployment-artifacts-to-app-env.ts +++ b/contracts/utils/deployment-artifacts-to-app-env.ts @@ -33,7 +33,6 @@ const argv = minimist(process.argv.slice(2), { const ZAddress = z.string().regex(/^0x[0-9a-fA-F]{40}$/); const ZDeploymentContext = z.object({ - deployedContracts: z.array(z.tuple([z.string(), ZAddress])), collateralContracts: z.array( z.object({ activePool: ZAddress, From 0ddb05574aa357cc5b8eb6d4c20e049bcf5e4032 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Wed, 4 Sep 2024 15:11:14 +0700 Subject: [PATCH 2/5] ci: increase timeout of deployment and make it verbose Now that it's multi-collateral, it takes longer to deploy. --- .github/workflows/testnet-deployment.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testnet-deployment.yml b/.github/workflows/testnet-deployment.yml index 7439a0ab..9d22f6f1 100644 --- a/.github/workflows/testnet-deployment.yml +++ b/.github/workflows/testnet-deployment.yml @@ -59,8 +59,8 @@ jobs: - name: Run deployment tool working-directory: ./contracts - run: ./deploy liquity-testnet --verify - timeout-minutes: 5 + run: ./deploy liquity-testnet --debug --verify + timeout-minutes: 10 env: DEPLOYER: ${{ secrets.DEPLOYER }} From 5562f8b01fc331f8fcb68518789618a7fff909a8 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Wed, 4 Sep 2024 16:19:51 +0700 Subject: [PATCH 3/5] feat: allow setting a gas price --- contracts/utils/deploy-cli.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/utils/deploy-cli.ts b/contracts/utils/deploy-cli.ts index 261f1099..90ea2943 100644 --- a/contracts/utils/deploy-cli.ts +++ b/contracts/utils/deploy-cli.ts @@ -72,6 +72,7 @@ const argv = minimist(process.argv.slice(2), { "rpc-url", "verifier", "verifier-url", + "gas-price", ], }); @@ -138,6 +139,11 @@ export async function main() { forgeArgs.push("--slow"); } + if (options.gasPrice) { + forgeArgs.push("--with-gas-price"); + forgeArgs.push(options.gasPrice); + } + // verify if (options.verify) { forgeArgs.push("--verify"); @@ -322,6 +328,7 @@ async function parseArgs() { verify: argv["verify"], verifier: argv["verifier"], verifierUrl: argv["verifier-url"], + gasPrice: argv["gas-price"], }; const [networkPreset] = argv._; From 93d337912bc58866166de66009a8bb1fd31c3cd0 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Wed, 4 Sep 2024 16:27:14 +0700 Subject: [PATCH 4/5] fix: add missing help for `--gas-price` --- contracts/utils/deploy-cli.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/utils/deploy-cli.ts b/contracts/utils/deploy-cli.ts index 90ea2943..d8a19720 100644 --- a/contracts/utils/deploy-cli.ts +++ b/contracts/utils/deploy-cli.ts @@ -22,11 +22,12 @@ Options: Requires a Ledger if an address is used. --ledger-path HD path to use with the Ledger (only used when DEPLOYER is an address). + --dry-run Don't broadcast transaction, only + simulate execution. --etherscan-api-key Etherscan API key to verify the contracts (required when verifying with Etherscan). + --gas-price Max fee per gas to use in transactions. --help, -h Show this help message. - --dry-run Don't broadcast transaction, only - simulate execution. --open-demo-troves Open demo troves after deployment (local only). --rpc-url RPC URL to use. From c4abbe3d0a1c80551cbf72eef9efe3cf0fa0908f Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Wed, 4 Sep 2024 19:03:06 +0700 Subject: [PATCH 5/5] chore: remove unused code --- contracts/utils/deploy-cli.ts | 54 ----------------------------------- 1 file changed, 54 deletions(-) diff --git a/contracts/utils/deploy-cli.ts b/contracts/utils/deploy-cli.ts index d8a19720..0582bb34 100644 --- a/contracts/utils/deploy-cli.ts +++ b/contracts/utils/deploy-cli.ts @@ -45,14 +45,6 @@ e.g. --chain-id can be set via CHAIN_ID instead. Parameters take precedence over const ANVIL_FIRST_ACCOUNT = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; -const PROTOCOL_CONTRACTS_VALID_NAMES = [ - "WETHTester", - "BoldToken", - "CollateralRegistry", - "HintHelpers", - "MultiTroveGetter", -]; - const argv = minimist(process.argv.slice(2), { alias: { h: "help", @@ -263,52 +255,6 @@ Deploying Liquity contracts with the following settings: echo(""); } -type Transaction = { transactionType: string }; - -function isDeploymentLog(log: unknown): log is { transactions: Transaction[] } { - return ( - typeof log === "object" - && log !== null - && "transactions" in log - && Array.isArray(log.transactions) - && (log.transactions as unknown[]) - .every((tx) => ( - typeof tx === "object" - && tx !== null - && "transactionType" in tx - && typeof tx.transactionType === "string" - )) - ); -} - -type ContractCreation = { - transactionType: "CREATE" | "CREATE2"; - contractName: string; - contractAddress: string; -}; - -function isContractCreation(tx: Transaction): tx is ContractCreation { - return ( - (tx.transactionType === "CREATE" || tx.transactionType === "CREATE2") - && "contractName" in tx - && typeof tx.contractName === "string" - && "contractAddress" in tx - && typeof tx.contractAddress === "string" - ); -} - -async function getDeployedContracts(jsonPath: string) { - const latestRun = await fs.readJson(jsonPath); - - if (isDeploymentLog(latestRun)) { - return latestRun.transactions - .filter(isContractCreation) - .map((tx) => [tx.contractName, tx.contractAddress]); - } - - throw new Error("Invalid deployment log: " + JSON.stringify(latestRun)); -} - function safeParseInt(value: string) { const parsed = parseInt(value, 10); return isNaN(parsed) ? undefined : parsed;