diff --git a/.github/workflows/contracts-tests.yml b/.github/workflows/contracts-tests.yml index 5a3377a0..7f815076 100644 --- a/.github/workflows/contracts-tests.yml +++ b/.github/workflows/contracts-tests.yml @@ -59,6 +59,7 @@ jobs: name: Foundry "E2E" tests runs-on: ubuntu-latest env: + SALT: Liquity2-E2E FORK_URL: ${{ secrets.MAINNET_RPC_URL }} FORK_CHAIN_ID: 1 FORK_BLOCK_NUMBER: 21571000 @@ -91,8 +92,11 @@ jobs: - name: Fork mainnet run: ./fork start && sleep 5 - - name: Deploy contracts - run: ./fork deploy + - name: Deploy BOLD + run: ./fork deploy --mode bold-only + + - name: Deploy everything else + run: ./fork deploy --mode use-existing-bold - name: Run E2E tests run: ./fork e2e -vvv @@ -219,15 +223,14 @@ jobs: # Filter - name: Filter out from coverage - run: - lcov --remove lcov_merged.info -o lcov_merged.info - 'test/*' - 'script/*' - 'src/Dependencies/Ownable.sol' - 'src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol' - 'src/NFTMetadata/*' - 'src/MultiTroveGetter.sol' - 'src/HintHelpers.sol' + run: lcov --remove lcov_merged.info -o lcov_merged.info + 'test/*' + 'script/*' + 'src/Dependencies/Ownable.sol' + 'src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol' + 'src/NFTMetadata/*' + 'src/MultiTroveGetter.sol' + 'src/HintHelpers.sol' # Send to coveralls - name: Coveralls diff --git a/contracts/.env.template b/contracts/.env.template index 26dda408..ada2f4d2 100644 --- a/contracts/.env.template +++ b/contracts/.env.template @@ -19,5 +19,10 @@ FORK_CHAIN_ID=1 # Defaults to Anvil account #0 # DEPLOYER= +# Deployment script will use `keccak256(bytes(...))` of this as salt when deploying via CREATE2 +# Defaults to `block.timestamp.toString()` +# Must be explicitly set when using 2-stage deployment (bold-only, use-existing-bold) +# SALT=Liquity2 + # Better call traces, e.g. when WETH/wstETH/RETH or Curve, etc. is involved # ETHERSCAN_API_KEY= diff --git a/contracts/fork b/contracts/fork index bb631986..f7ba47cd 100755 --- a/contracts/fork +++ b/contracts/fork @@ -96,7 +96,7 @@ deploy() { ) # sourcify_start - "${deploy[@]}" + "${deploy[@]}" "$@" # sourcify_stop } @@ -131,7 +131,7 @@ case $cmd in deploy) ensure_running init_env - deploy + deploy "$@" ;; e2e) ensure_running diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index ebdec5d2..ec990689 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -30,6 +30,8 @@ import "src/PriceFeeds/RETHPriceFeed.sol"; import "src/CollateralRegistry.sol"; import "test/TestContracts/PriceFeedTestnet.sol"; import "test/TestContracts/MetadataDeployment.sol"; +import "test/Utils/Logging.sol"; +import "test/Utils/StringEquality.sol"; import "src/Zappers/WETHZapper.sol"; import "src/Zappers/GasCompZapper.sol"; import "src/Zappers/LeverageLSTZapper.sol"; @@ -52,9 +54,14 @@ import {MockStakingV1} from "V2-gov/test/mocks/MockStakingV1.sol"; import {DeployGovernance} from "./DeployGovernance.s.sol"; -contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, MetadataDeployment { +contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, MetadataDeployment, Logging { using Strings for *; using StringFormatting for *; + using StringEquality for string; + + string constant DEPLOYMENT_MODE_COMPLETE = "complete"; + string constant DEPLOYMENT_MODE_BOLD_ONLY = "bold-only"; + string constant DEPLOYMENT_MODE_USE_EXISTING_BOLD = "use-existing-bold"; address WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; @@ -213,7 +220,8 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, } function run() external { - SALT = keccak256(abi.encodePacked(block.timestamp)); + string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); + SALT = keccak256(bytes(saltStr)); if (vm.envBytes("DEPLOYER").length == 20) { // address @@ -226,11 +234,43 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, vm.startBroadcast(privateKey); } + string memory deploymentMode = vm.envOr("DEPLOYMENT_MODE", DEPLOYMENT_MODE_COMPLETE); + require( + deploymentMode.eq(DEPLOYMENT_MODE_COMPLETE) || deploymentMode.eq(DEPLOYMENT_MODE_BOLD_ONLY) + || deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD), + string.concat("Bad deployment mode: ", deploymentMode) + ); + useTestnetPriceFeeds = vm.envOr("USE_TESTNET_PRICEFEEDS", false); - console2.log(deployer, "deployer"); - console2.log(deployer.balance, "deployer balance"); - console2.log("Use Testnet PriceFeeds: ", useTestnetPriceFeeds); + _log("Deployer: ", deployer.toHexString()); + _log("Deployer balance: ", deployer.balance.decimal()); + _log("Deployment mode: ", deploymentMode); + _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); + _log("Use testnet PriceFeeds: ", useTestnetPriceFeeds ? "yes" : "no"); + + // Deploy Bold or pick up existing deployment + bytes memory boldBytecode = bytes.concat(type(BoldToken).creationCode, abi.encode(deployer)); + address boldAddress = vm.computeCreate2Address(SALT, keccak256(boldBytecode)); + BoldToken boldToken; + + if (deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD)) { + require(boldAddress.code.length > 0, string.concat("BOLD not found at ", boldAddress.toHexString())); + boldToken = BoldToken(boldAddress); + + // Check BOLD is untouched + require(boldToken.totalSupply() == 0, "Some BOLD has been minted!"); + require(boldToken.collateralRegistryAddress() == address(0), "Collateral registry already set"); + require(boldToken.owner() == deployer, "Not BOLD owner"); + } else { + boldToken = new BoldToken{salt: SALT}(deployer); + assert(address(boldToken) == boldAddress); + } + + if (deploymentMode.eq(DEPLOYMENT_MODE_BOLD_ONLY)) { + vm.writeFile("deployment-manifest.json", string.concat('{"boldToken":"', boldAddress.toHexString(), '"}')); + return; + } if (block.chainid == 1) { // mainnet @@ -289,7 +329,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, stakingV1: stakingV1, lqty: lqty, lusd: lusd, - bold: address(0) // not yet known; will be set by `_deployAndConnectContracts()` + bold: boldAddress }); DeploymentResult memory deployed = @@ -466,12 +506,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, DeploymentVars 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)); - r.boldToken = new BoldToken{salt: SALT}(deployer); - assert(address(r.boldToken) == vars.boldTokenAddress); - _deployGovernanceParams.bold = address(r.boldToken); + r.boldToken = BoldToken(_deployGovernanceParams.bold); // USDC and USDC-BOLD pool r.usdcCurvePool = _deployCurveBoldUsdcPool(r.boldToken); diff --git a/contracts/test/E2E.t.sol b/contracts/test/E2E.t.sol index fb523e3a..8cd25c91 100644 --- a/contracts/test/E2E.t.sol +++ b/contracts/test/E2E.t.sol @@ -35,6 +35,7 @@ import {IPriceFeedV1} from "./Interfaces/LiquityV1/IPriceFeedV1.sol"; import {ISortedTrovesV1} from "./Interfaces/LiquityV1/ISortedTrovesV1.sol"; import {ITroveManagerV1} from "./Interfaces/LiquityV1/ITroveManagerV1.sol"; import {ERC20Faucet} from "./TestContracts/ERC20Faucet.sol"; +import {StringEquality} from "./Utils/StringEquality.sol"; address constant ETH_WHALE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // Anvil account #1 address constant WETH_WHALE = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; // Anvil account #2 @@ -109,16 +110,6 @@ library SideEffectFreeGetPrice { } } -library StringEquality { - function eq(string memory a, string memory b) internal pure returns (bool) { - return keccak256(bytes(a)) == keccak256(bytes(b)); - } - - function notEq(string memory a, string memory b) internal pure returns (bool) { - return !eq(a, b); - } -} - contract E2ETest is Test { using SideEffectFreeGetPrice for IPriceFeed; using SideEffectFreeGetPrice for IPriceFeedV1; diff --git a/contracts/test/Utils/StringEquality.sol b/contracts/test/Utils/StringEquality.sol new file mode 100644 index 00000000..95d0ffa2 --- /dev/null +++ b/contracts/test/Utils/StringEquality.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +library StringEquality { + function eq(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); + } + + function notEq(string memory a, string memory b) internal pure returns (bool) { + return !eq(a, b); + } +} diff --git a/contracts/utils/deploy-cli.ts b/contracts/utils/deploy-cli.ts index 5ab4510f..344b3e5b 100644 --- a/contracts/utils/deploy-cli.ts +++ b/contracts/utils/deploy-cli.ts @@ -28,6 +28,11 @@ Options: (required when verifying with Etherscan). --gas-price Max fee per gas to use in transactions. --help, -h Show this help message. + --mode Deploy in one of the following modes: + - complete (default) + - bold-only + - use-existing-bold + --salt SALT used for CREATE2 --open-demo-troves Open demo troves after deployment (local only). --rpc-url RPC URL to use. @@ -65,6 +70,8 @@ const argv = minimist(process.argv.slice(2), { "deployer", "etherscan-api-key", "ledger-path", + "mode", + "salt", "rpc-url", "verifier", "verifier-url", @@ -100,6 +107,7 @@ export async function main() { options.chainId ??= 1; } + options.mode ??= "complete"; options.verifier ??= "etherscan"; // handle missing options @@ -177,8 +185,10 @@ Deploying Liquity contracts with the following settings: CHAIN_ID: ${options.chainId} DEPLOYER: ${options.deployer} - LEDGER_PATH: ${options.ledgerPath} + DEPLOYMENT_MODE: ${options.mode} + SALT: ${options.salt ? options.salt : "\u26A0 block.timestamp will be used !!"} ETHERSCAN_API_KEY: ${options.etherscanApiKey && "(secret)"} + LEDGER_PATH: ${options.ledgerPath} OPEN_DEMO_TROVES: ${options.openDemoTroves ? "yes" : "no"} RPC_URL: ${options.rpcUrl} USE_TESTNET_PRICEFEEDS: ${options.useTestnetPricefeeds ? "yes" : "no"} @@ -188,6 +198,11 @@ Deploying Liquity contracts with the following settings: `; process.env.DEPLOYER = options.deployer; + process.env.DEPLOYMENT_MODE = options.mode; + + if (options.salt) { + process.env.SALT = options.salt; + } if (options.openDemoTroves) { process.env.OPEN_DEMO_TROVES = "true"; @@ -218,6 +233,11 @@ Deploying Liquity contracts with the following settings: multiTroveGetter: string; }; + if (options.mode === "bold-only") { + echo("BoldToken address:", deploymentManifest.boldToken); + return; + } + const protocolContracts = { BoldToken: deploymentManifest.boldToken, CollateralRegistry: deploymentManifest.collateralRegistry, @@ -297,6 +317,8 @@ async function parseArgs() { etherscanApiKey: argv["etherscan-api-key"], help: argv["help"], ledgerPath: argv["ledger-path"], + mode: argv["mode"], + salt: argv["salt"], openDemoTroves: argv["open-demo-troves"], rpcUrl: argv["rpc-url"], dryRun: argv["dry-run"], @@ -317,6 +339,7 @@ async function parseArgs() { options.deployer ??= process.env.DEPLOYER; options.etherscanApiKey ??= process.env.ETHERSCAN_API_KEY; options.ledgerPath ??= process.env.LEDGER_PATH; + options.mode ??= process.env.DEPLOYMENT_MODE; options.openDemoTroves ??= Boolean( process.env.OPEN_DEMO_TROVES && process.env.OPEN_DEMO_TROVES !== "false", );