Source code for tradeexecutor.cli.commands.enzyme_deploy_vault

"""enzyme-deploy-vault CLI command.

See :ref:`vault deployment` for the full documentation how to use this command.

Example how to manually test:

.. code-block:: shell

    export SIMULATE=true
    export FUND_NAME="Up only and then more"
    export FUND_SYMBOL="UP"
    export TERMS_OF_SERVICE_ADDRESS="0x24BB78E70bE0fC8e93Ce90cc8A586e48428Ff515"
    export VAULT_RECORD_FILE="/tmp/sample-vault-deployment.json"
    export OWNER_ADDRESS="0x238B0435F69355e623d99363d58F7ba49C408491"

    #
    # Asset configuration
    #

    # USDC
    export DENOMINATION_ASSET="0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
    # Whitelisted tokens for Polygon: WETH, WMATIC
    export WHITELISTED_ASSETS="0x7ceb23fd6bc0add59e62ac25578270cff1b9f619 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"

    #
    # Secret configuration
    #

    export JSON_RPC_POLYGON=
    export PRIVATE_KEY=
    # Is Polygonscan.com API key, passed to Forge
    export ETHERSCAN_API_KEY=

    trade-executor enzyme-deploy-vault
"""

import json
import os.path
import sys
from pathlib import Path
from pprint import pformat
from typing import Optional

from typer import Option

from eth_defi.abi import get_deployed_contract
from eth_defi.deploy import deploy_contract
from eth_defi.enzyme.deployment import POLYGON_DEPLOYMENT, EnzymeDeployment
from eth_defi.enzyme.generic_adapter_vault import deploy_vault_with_generic_adapter
from eth_defi.hotwallet import HotWallet
from eth_defi.token import fetch_erc20_details
from tradeexecutor.monkeypatch.web3 import construct_sign_and_send_raw_middleware
from tradingstrategy.chain import ChainId

from tradeexecutor.cli.bootstrap import create_web3_config
from tradeexecutor.cli.commands import shared_options
from tradeexecutor.cli.commands.app import app
from tradeexecutor.cli.log import setup_logging


[docs]@app.command() def enzyme_deploy_vault( log_level: str = shared_options.log_level, json_rpc_binance: Optional[str] = shared_options.json_rpc_binance, json_rpc_polygon: Optional[str] = shared_options.json_rpc_polygon, json_rpc_avalanche: Optional[str] = shared_options.json_rpc_avalanche, json_rpc_ethereum: Optional[str] = shared_options.json_rpc_ethereum, json_rpc_arbitrum: Optional[str] = shared_options.json_rpc_arbitrum, json_rpc_anvil: Optional[str] = shared_options.json_rpc_anvil, private_key: str = shared_options.private_key, # Vault options vault_record_file: Optional[Path] = Option(..., envvar="VAULT_RECORD_FILE", help="Store vault and comptroller addresses in this JSON file. It's important to write down all contract addresses."), fund_name: Optional[str] = Option(..., envvar="FUND_NAME", help="On-chain name for the fund shares"), fund_symbol: Optional[str] = Option(..., envvar="FUND_SYMBOL", help="On-chain token symbol for the fund shares"), comptroller_lib: Optional[str] = Option(None, envvar="COMPTROLLER_LIB", help="Enzyme's ComptrollerLib address for custom deployments"), denomination_asset: Optional[str] = Option(None, envvar="DENOMINATION_ASSET", help="Stablecoin asset used for vault denomination"), owner_address: Optional[str] = Option(None, envvar="OWNER_ADDRESS", help="The protocol or multisig address that is set as the owner of the vault"), terms_of_service_address: Optional[str] = Option(None, envvar="TERMS_OF_SERVICE_ADDRESS", help="The address of the terms of service smart contract"), whitelisted_assets: Optional[str] = Option(..., envvar="WHITELISTED_ASSETS", help="Space separarted list of ERC-20 addresses this vault can trade. Denomination asset does not need to be whitelisted separately."), unit_testing: bool = shared_options.unit_testing, production: bool = Option(False, envvar="PRODUCTION", help="Set production metadata flag true for the deployment."), simulate: bool = Option(False, envvar="SIMULATE", help="Simulate deployment using Anvil mainnet work, when doing manual deployment testing."), etherscan_api_key: Optional[str] = Option(None, envvar="ETHERSCAN_API_KEY", help="Etherscan API key need to verify the contracts on a production deployment."), ): """Deploy a new Enzyme vault. Deploys a new Enzyme vault that is configured to be run with Trading Strategy Protocol. Multiple contracts will be deployed and verified in a blockchain explorer. The vault is configured with custom guard, deposit and terms of service contracts. """ logger = setup_logging(log_level) web3config = create_web3_config( json_rpc_binance=json_rpc_binance, json_rpc_polygon=json_rpc_polygon, json_rpc_avalanche=json_rpc_avalanche, json_rpc_ethereum=json_rpc_ethereum, json_rpc_anvil=json_rpc_anvil, json_rpc_arbitrum=json_rpc_arbitrum, simulate=simulate, ) if not web3config.has_any_connection(): raise RuntimeError("Vault deploy requires that you pass JSON-RPC connection to one of the networks") web3config.choose_single_chain() web3 = web3config.get_default() chain_id = ChainId(web3.eth.chain_id) logger.info("Connected to chain %s", chain_id.name) hot_wallet = HotWallet.from_private_key(private_key) hot_wallet.sync_nonce(web3) web3.middleware_onion.add(construct_sign_and_send_raw_middleware(hot_wallet.account)) # Build the list of whitelisted assets GuardV0 allows us to trade whitelisted_asset_details = [] for token_address in whitelisted_assets.split(): token_address = token_address.strip() if token_address: whitelisted_asset_details.append(fetch_erc20_details(web3, token_address)) assert len(whitelisted_asset_details) >= 1, "You need to whitelist at least one token as a trading pair" if whitelisted_asset_details[0].symbol == "USDC": # Unit test path usdc = whitelisted_asset_details[0].contract whitelisted_asset_details = whitelisted_asset_details[1:] else: # Will read from the chain usdc = None # No other supported Enzyme deployments match chain_id: case ChainId.ethereum: raise NotImplementedError("Not supported yet") case ChainId.polygon: deployment_info = POLYGON_DEPLOYMENT enzyme_deployment = EnzymeDeployment.fetch_deployment(web3, POLYGON_DEPLOYMENT, deployer=hot_wallet.address) denomination_token = fetch_erc20_details(web3, deployment_info["usdc"]) case _: assert comptroller_lib, f"You need to give Enzyme's ComptrollerLib address for a chain {chain_id}" assert denomination_asset, f"You need to give denomination_asset for a chain {chain_id}" enzyme_deployment = EnzymeDeployment.fetch_deployment(web3, {"comptroller_lib": comptroller_lib}, deployer=hot_wallet.address) denomination_token = fetch_erc20_details(web3, denomination_asset) # Check the chain is online logger.info(f" Chain id is {web3.eth.chain_id:,}") logger.info(f" Latest block is {web3.eth.block_number:,}") if terms_of_service_address is not None: terms_of_service = get_deployed_contract( web3, "terms-of-service/TermsOfService.json", terms_of_service_address, ) terms_of_service.functions.latestTermsOfServiceVersion().call() # Check ABI matches or crash else: terms_of_service = None asset_manager_address = hot_wallet.address if owner_address is None: owner_address = hot_wallet.address if simulate: logger.info("Simulation deployment") else: logger.info("Ready to deploy") logger.info("-" * 80) logger.info("Deployer hot wallet: %s", hot_wallet.address) logger.info("Deployer balance: %f, nonce %d", hot_wallet.get_native_currency_balance(web3), hot_wallet.current_nonce) logger.info("Enzyme FundDeployer: %s", enzyme_deployment.contracts.fund_deployer.address) if enzyme_deployment.usdc is not None: logger.info("USDC: %s", enzyme_deployment.usdc.address) logger.info("Terms of service: %s", terms_of_service.address if terms_of_service else "-") logger.info("Fund: %s (%s)", fund_name, fund_symbol) logger.info("Whitelisted assets: %s", ", ".join([a.symbol for a in whitelisted_asset_details])) if owner_address != hot_wallet.address: logger.info("Ownership will be transferred to %s", owner_address) else: logger.warning("Ownership will be retained at the deployer %s", hot_wallet.address) if asset_manager_address != hot_wallet.address: logger.info("Asset manager is %s", asset_manager_address) else: logger.warning("No separate asset manager role set: will use the current hot wallet as the asset manager") logger.info("-" * 80) if not (simulate or unit_testing): confirm = input("Ok [y/n]? ") if not confirm.lower().startswith("y"): print("Aborted") sys.exit(1) try: # Currently assumes HotWallet = asset manager # as the trade-executor that deploys the vault is going to # the assset manager for this vault vault = deploy_vault_with_generic_adapter( enzyme_deployment, hot_wallet, asset_manager=hot_wallet.address, owner=owner_address, terms_of_service=terms_of_service, denomination_asset=denomination_token.contract, fund_name=fund_name, fund_symbol=fund_symbol, whitelisted_assets=whitelisted_asset_details, etherscan_api_key=etherscan_api_key if not simulate else None, # Only verify when not simulating production=production, ) except Exception as e: raise RuntimeError(f"Deployment failed. Hot wallet: {hot_wallet.address}, denomination asset: {denomination_token.address}") from e if vault_record_file: # Make a small file, mostly used to communicate with unit tests with open(vault_record_file, "wt") as out: vault_record = { "fund_name": fund_name, "fund_symbol": fund_symbol, "vault": vault.address, "comptroller": vault.comptroller.address, "generic_adapter": vault.generic_adapter.address, "block_number": vault.deployed_at_block, "usdc_payment_forwarder": vault.payment_forwarder.address, "guard": vault.guard_contract.address, "deployer": hot_wallet.address, "denomination_token": denomination_token.address, "terms_of_service": terms_of_service_address, "whitelisted_assets": whitelisted_assets, "asset_manager_address": asset_manager_address, "owner_address": owner_address, } json.dump(vault_record, out, indent=4) logger.info("Wrote %s for vault details", os.path.abspath(vault_record_file)) logger.info("Vault environment variables for trade-executor init command:\n%s", pformat(vault.get_deployment_info())) web3config.close()