diff --git a/Makefile b/Makefile index 0cad5ce43..9d5cfb5f1 100644 --- a/Makefile +++ b/Makefile @@ -87,3 +87,6 @@ prettier: deploy: ./scripts/deploy.sh + +generate-deploy: + ./scripts/generate-deploy-config.sh diff --git a/README.md b/README.md index 42a8f4f8f..c3f3520fe 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,6 @@ If you want to automatically format the code, run: make prettier ``` - ## Deploy To deploy the smart contracts, run: @@ -127,6 +126,28 @@ To deploy the smart contracts, run: NETWORK= make deploy ``` +### Generate Deploy Configurations + +Factory, coordinator, and instance deploy configurations can be generated via templates. + +To get started, create a new variable file from the sample by running: + +```sh +cp \ + tasks/deploy/config/.sample.env \ + .env +``` + +Next, set values for each of the fields in the file `.env`. Sample values are provided as examples, but should be overwritten. + +Finally, to generate the configuration files run: + +```sh +make generate-deploy +``` + +The above command will output next steps to integrate the new configuration files with existing configuration. + # Disclaimer The language used in this code and documentation is not intended to, and does not, have any particular financial, legal, or regulatory significance. diff --git a/scripts/generate-deploy-config.sh b/scripts/generate-deploy-config.sh new file mode 100755 index 000000000..d5fb4ef69 --- /dev/null +++ b/scripts/generate-deploy-config.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +set -e + +config_dir=tasks/deploy/config + +# Store names of generated configurations for integration instructions at end of script. +factories=() +coordinators=() +instances=() + +# Non-destructively updates the `index.ts` file for the network and global config. +function update_indexes() { + local network=$1 + echo " - updating $config_dir/$network/index.ts" + echo "" >"$config_dir/$network/index.ts" + for file in "$config_dir/$network"/*; do + base=$(basename $file) + if [[ "$base" != 'index.ts' ]]; then + echo "export * from \"./$(echo $base | cut -d"." -f1)\";" >>"$config_dir/$network/index.ts" + fi + done + network_export="$(grep -i "$network" <"$config_dir/index.ts" || true)" + if [[ -z $network_export ]]; then + echo " - updating $config_dir/index.ts" + echo "export * from \"./$network\";" >>"$config_dir/index.ts" + fi +} + +# Generates factory from `factory.env` if present, see `echo` statements for context. +factory_input=factory.env +factory_template=$config_dir/factory.ts.tmpl +factory_output_filename=factory.ts +if [ -f $factory_input ]; then + echo "generating factory configuration..." + echo " - reading configuration from ${factory_input}" + export $(grep -v '^#' $factory_input | xargs) + + # Skip generation if output file already exists. + factory_output_dir="$config_dir/$NETWORK_NAME" + factory_output_file="$factory_output_dir/$factory_output_filename" + if [ -f $factory_output_file ]; then + echo " - skipping generation, factory exists for network $NETWORK_NAME" + else + echo " - writing configuration to ${factory_output_file}" + mkdir -p $factory_output_dir + envsubst <$factory_template >$factory_output_file + update_indexes $NETWORK_NAME + factories+="${NETWORK_NAME}_FACTORY" + fi + echo "done! \n" +fi + +# Generates coordinator from `coordinator.env` if present, see `echo` statements for context. +coordinator_input=coordinator.env +coordinator_template=$config_dir/coordinator.ts.tmpl +if [ -f $coordinator_input ]; then + echo "generating coordinator configuration..." + echo " - reading configuration from ${coordinator_input}" + export $(grep -v '^#' $coordinator_input | xargs) + + # Skip generation if output file already exists. + coordinator_output_filename="$NAME.ts" + coordinator_output_dir="$config_dir/$NETWORK_NAME" + coordinator_output_file="$coordinator_output_dir/$coordinator_output_filename" + if [ -f $coordinator_output_file ]; then + echo " - skipping generation, coordinator exists for network $NETWORK_NAME" + else + echo " - writing configuration to ${coordinator_output_file}" + mkdir -p $coordinator_output_dir + envsubst <$coordinator_template >$coordinator_output_file + update_indexes $NETWORK_NAME + coordinators+="$NAME" + fi + echo "done! \n" +fi + +# Generates instance from `instance.env` if present, see `echo` statements for context. +instance_input=instance.env +instance_template=$config_dir/instance.ts.tmpl +if [ -f $instance_input ]; then + echo "generating instance configuration..." + echo " - reading configuration from ${instance_input}" + export $(grep -v '^#' $instance_input | xargs) + + # Skip generation if output file already exists. + instance_output_filename="$NAME.ts" + instance_output_dir="$config_dir/$NETWORK_NAME" + instance_output_file="$instance_output_dir/$instance_output_filename" + if [ -f $instance_output_file ]; then + echo " - skipping generation, instance exists for network $NETWORK_NAME" + else + echo " - writing configuration to ${instance_output_file}" + mkdir -p $instance_output_dir + envsubst <$instance_template >$instance_output_file + update_indexes $NETWORK_NAME + instances+="$NAME" + fi + echo "done! \n" +fi + +# Outputs instructions for integrating the generated configurations with +# existing deploy configurations in `hardhat.config.ts`. +names=(${factories[@]} ${coordinators[@]} ${instances[@]}) +IFS=,\n +if [ ! ${#names[@]} -eq 0 ]; then + echo " + All configurations have been generated. + + Add: + + ${names[*]} + + to the import from \"./tasks/deploy/config/\" in hardhat.config.ts + + Then, merge the following into the network configuration for \"${NETWORK_NAME}\". + " + if [ ! ${#factories[@]} -eq 0 ]; then + echo " + factories: [ + ${factories[*]} + ] + " + fi + if [ ! ${#coordinators[@]} -eq 0 ]; then + echo " + coordinators: [ + ${coordinators[*]} + ] + " + fi + if [ ! ${#instances[@]} -eq 0 ]; then + echo " + instances: [ + ${instances[*]} + ] + " + fi +else + echo "No configuration was generated." +fi diff --git a/tasks/deploy/config/coordinator.sample.env b/tasks/deploy/config/coordinator.sample.env new file mode 100644 index 000000000..a8c79b55e --- /dev/null +++ b/tasks/deploy/config/coordinator.sample.env @@ -0,0 +1,10 @@ +NETWORK_NAME= +NAME=_COORDINATOR +PREFIX= + +# Token address to include in the constructor for non-ERC4626 coordinators. +VAULT_SHARES_TOKEN_ADDRESS=0x... + +# Extra argument passed to coordinator (and all target deployer) constructors. +# Only necessary for coordinators with prefix 'EzETH'. +EXTRA_CONSTRUCTOR_ARG=0xd94a3A0BfC798b98a700a785D5C610E8a2d5DBD8 diff --git a/tasks/deploy/config/coordinator.ts.tmpl b/tasks/deploy/config/coordinator.ts.tmpl new file mode 100644 index 000000000..48353478a --- /dev/null +++ b/tasks/deploy/config/coordinator.ts.tmpl @@ -0,0 +1,11 @@ +import { HyperdriveCoordinatorConfig } from "../../lib"; + +export const ${NAME}_COORDINATOR: HyperdriveCoordinatorConfig<"$PREFIX"> = { + name: "${NAME}_COORDINATOR".toUpperCase(), + prefix: "$PREFIX", + factoryAddress: async (hre) => + hre.hyperdriveDeploy.deployments.byName("FACTORY").address, + targetCount: 4, + extraConstructorArgs: ["$EXTRA_CONSTRUCTOR_ARG"], + token: "$PREFIX" === "ERC4626" ? undefined : "$VAULT_SHARES_TOKEN_ADDRESS", +}; diff --git a/tasks/deploy/config/factory.sample.env b/tasks/deploy/config/factory.sample.env new file mode 100644 index 000000000..a28881d80 --- /dev/null +++ b/tasks/deploy/config/factory.sample.env @@ -0,0 +1,21 @@ +NETWORK_NAME= +GOV_ADDRESS=0x... +CHECKPOINT_DURATION_RESOLUTION_HOURS=8 +MIN_CHECKPOINT_DURATION_HOURS=24 +MAX_CHECKPOINT_DURATION_HOURS=24 +MIN_POSITION_DURATION_DAYS=7 +MAX_POSITION_DURATION_DAYS=365 +MIN_FIXED_APR=0.01 +MAX_FIXED_APR=0.6 +MIN_TIMESTRETCH_APR=0.01 +MAX_TIMESTRETCH_APR=0.6 +MIN_CIRCUIT_BREAKER_DELTA=0.5 +MAX_CIRCUIT_BREAKER_DELTA=1 +MIN_CURVE_FEE=0.001 +MAX_CURVE_FEE=0.0001 +MIN_FLAT_FEE=0.15 +MAX_FLAT_FEE=0.03 +MIN_GOV_LP_FEE=0.05 +MAX_GOV_LP_FEE=0.005 +MIN_GOV_ZOMBIE_FEE=0.15 +MAX_GOV_ZOMBIE_FEE=0.03 diff --git a/tasks/deploy/config/factory.ts.tmpl b/tasks/deploy/config/factory.ts.tmpl new file mode 100644 index 000000000..53b86a4c3 --- /dev/null +++ b/tasks/deploy/config/factory.ts.tmpl @@ -0,0 +1,66 @@ +import { parseEther } from "viem"; +import { HyperdriveFactoryConfig, parseDuration } from "../../lib"; + +export const ${NETWORK_NAME}_FACTORY: HyperdriveFactoryConfig = { + name: "FACTORY", + prepare: async (hre, options) => { + await hre.hyperdriveDeploy.ensureDeployed( + "FACTORY_FORWARDER", + "ERC20ForwarderFactory", + [], + options, + ); + }, + constructorArguments: async (hre) => [ + { + governance: "$GOV_ADDRESS", + deployerCoordinatorManager: (await hre.getNamedAccounts())['deployer'], + hyperdriveGovernance: "$GOV_ADDRESS", + defaultPausers: ["$GOV_ADDRESS"], + feeCollector: "$GOV_ADDRESS", + sweepCollector: "$GOV_ADDRESS", + checkpointDurationResolution: parseDuration( + "$CHECKPOINT_DURATION_RESOLUTION_HOURS hours", + ), + minCheckpointDuration: parseDuration( + "$MIN_CHECKPOINT_DURATION_HOURS hours", + ), + maxCheckpointDuration: parseDuration( + "$MAX_CHECKPOINT_DURATION_HOURS hours", + ), + minPositionDuration: parseDuration( + "$MIN_POSITION_DURATION_DAYS days", + ), + maxPositionDuration: parseDuration("$MAX_POSITION_DURATION_DAYS days"), + minFixedAPR: parseEther("$MIN_FIXED_APR"), + maxFixedAPR: parseEther("$MAX_FIXED_APR"), + minTimeStretchAPR: parseEther("$MIN_TIMESTRETCH_APR"), + maxTimeStretchAPR: parseEther("$MAX_FIXED_APR"), + minCircuitBreakerDelta: parseEther("$MIN_CIRCUIT_BREAKER_DELTA"), + maxCircuitBreakerDelta: parseEther("$MAX_CIRCUIT_BREAKER_DELTA"), + minFees: { + curve: parseEther("$MIN_CURVE_FEE"), + flat: parseEther("$MIN_FLAT_FEE"), + governanceLP: parseEther("$MIN_GOV_LP_FEE"), + governanceZombie: parseEther("$MIN_GOV_ZOMBIE_FEE"), + }, + maxFees: { + curve: parseEther("$MAX_CURVE_FEE"), + flat: parseEther("$MAX_FLAT_FEE"), + governanceLP: parseEther("$MAX_GOV_LP_FEE"), + governanceZombie: parseEther("$MAX_GOV_ZOMBIE_FEE"), + }, + linkerFactory: + hre.hyperdriveDeploy.deployments.byName("FACTORY_FORWARDER") + .address, + linkerCodeHash: await ( + await hre.viem.getContractAt( + "ERC20ForwarderFactory", + hre.hyperdriveDeploy.deployments.byName("FACTORY_FORWARDER") + .address, + ) + ).read.ERC20LINK_HASH(), + }, + "FACTORY", + ], +}; diff --git a/tasks/deploy/config/instance.sample.env b/tasks/deploy/config/instance.sample.env new file mode 100644 index 000000000..5216b9274 --- /dev/null +++ b/tasks/deploy/config/instance.sample.env @@ -0,0 +1,20 @@ +NETWORK_NAME= +NAME=- +PREFIX= +COORDINATOR_NAME= +SALT=0x... +CONTRIBUTION=500 +FIXED_APR=0.05 +TIMESTRETCH_APR=0.05 +AS_BASE=false +BASE_TOKEN=0x... +VAULT_SHARES_TOKEN=0x... +CIRCUIT_BREAKER_DELTA=0.6 +MIN_SHARE_RESERVES=0.001 +MIN_TX_AMOUNT=0.001 +POSITION_DURATION_DAYS=30 +CHECKPOINT_DURATION_DAYS=1 +CURVE_FEE=0.01 +FLAT_FEE=0.0005 +GOV_LP_FEE=0.15 +GOV_ZOMBIE_FEE=0.03 diff --git a/tasks/deploy/config/instance.ts.tmpl b/tasks/deploy/config/instance.ts.tmpl new file mode 100644 index 000000000..d819a240b --- /dev/null +++ b/tasks/deploy/config/instance.ts.tmpl @@ -0,0 +1,56 @@ +import { parseEther } from "viem"; +import { + HyperdriveInstanceConfig, + getLinkerDetails, + normalizeFee, + parseDuration, + toBytes32, +} from "../../lib"; + +const CONTRIBUTION = parseEther("$CONTRIBUTION"); + +export const $NAME: HyperdriveInstanceConfig<"$PREFIX"> = { + name: "$NAME", + prefix: "$PREFIX", + coordinatorAddress: async (hre) => + hre.hyperdriveDeploy.deployments.byName("$COORDINATOR_NAME").address, + deploymentId: toBytes32("$NAME"), + salt: toBytes32("$SALT"), + extraData: "0x", + contribution: CONTRIBUTION, + fixedAPR: parseEther("$FIXED_APR"), + timestretchAPR: parseEther("$TIMESTRETCH_APR"), + options: async (hre) => ({ + destination: (await hre.getNamedAccounts())['deployer'] as any, + asBase: $AS_BASE, + extraData: "0x", + }), + poolDeployConfig: async (hre) => { + let factoryAddress = hre.hyperdriveDeploy.deployments.byName("FACTORY").address; + let factoryContract = await hre.viem.getContractAt("HyperdriveFactory", factoryAddress); + let govAddress = await factoryContract.read.governance(); + return { + baseToken: "$BASE_TOKEN", + vaultSharesToken: "$VAULT_SHARES_TOKEN", + circuitBreakerDelta: parseEther("$CIRCUIT_BREAKER_DELTA"), + minimumShareReserves: parseEther("$MIN_SHARE_RESERVES"), + minimumTransactionAmount: parseEther("$MIN_TX_AMOUNT"), + positionDuration: parseDuration("$POSITION_DURATION_DAYS days"), + checkpointDuration: parseDuration("$CHECKPOINT_DURATION_DAYS days"), + timeStretch: 0n, + governance: govAddress, + feeCollector: govAddress, + sweepCollector: govAddress, + ...(await getLinkerDetails( + hre, + factoryAddress, + )), + fees: { + curve: parseEther("$CURVE_FEE"), + flat: normalizeFee(parseEther("FLAT_FEE"), "$POSITION_DURATION_DAYS days"), + governanceLP: parseEther("GOV_LP_FEE"), + governanceZombie: parseEther("GOV_ZOMBIE_FEE"), + }, + }; + }, +}; diff --git a/tasks/deploy/config/sepolia/erc4626-coordinator.ts b/tasks/deploy/config/sepolia/erc4626-coordinator.ts index 74b39a338..b77d9f0ba 100644 --- a/tasks/deploy/config/sepolia/erc4626-coordinator.ts +++ b/tasks/deploy/config/sepolia/erc4626-coordinator.ts @@ -5,7 +5,6 @@ export const SEPOLIA_ERC4626_COORDINATOR: HyperdriveCoordinatorConfig<"ERC4626"> name: "ERC4626_COORDINATOR", prefix: "ERC4626", targetCount: 4, - extraConstructorArgs: [], factoryAddress: async (hre) => hre.hyperdriveDeploy.deployments.byName("FACTORY").address, }; diff --git a/tasks/deploy/config/sepolia/reth-coordinator.ts b/tasks/deploy/config/sepolia/reth-coordinator.ts index 404a5eca4..4b5cc23d9 100644 --- a/tasks/deploy/config/sepolia/reth-coordinator.ts +++ b/tasks/deploy/config/sepolia/reth-coordinator.ts @@ -7,7 +7,6 @@ export const SEPOLIA_RETH_COORDINATOR: HyperdriveCoordinatorConfig<"RETH"> = { factoryAddress: async (hre) => hre.hyperdriveDeploy.deployments.byName("FACTORY").address, targetCount: 4, - extraConstructorArgs: [], prepare: async (hre, options) => { let pc = await hre.viem.getPublicClient(); let deployer = (await hre.getNamedAccounts())["deployer"]; diff --git a/tasks/deploy/config/sepolia/steth-coordinator.ts b/tasks/deploy/config/sepolia/steth-coordinator.ts index a41ab4504..ceba777ec 100644 --- a/tasks/deploy/config/sepolia/steth-coordinator.ts +++ b/tasks/deploy/config/sepolia/steth-coordinator.ts @@ -7,7 +7,6 @@ export const SEPOLIA_STETH_COORDINATOR: HyperdriveCoordinatorConfig<"StETH"> = { factoryAddress: async (hre) => hre.hyperdriveDeploy.deployments.byName("FACTORY").address, targetCount: 4, - extraConstructorArgs: [], prepare: async (hre, options) => { let deployer = (await hre.getNamedAccounts())["deployer"]; let pc = await hre.viem.getPublicClient();