"""lagoon-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 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 typing import Optional
from typer import Option
from eth_defi.aave_v3.constants import AAVE_V3_DEPLOYMENTS
from eth_defi.hotwallet import HotWallet
from eth_defi.lagoon.deployment import LagoonDeploymentParameters, deploy_automated_lagoon_vault, DEFAULT_PERFORMANCE_RATE, DEFAULT_MANAGEMENT_RATE
from eth_defi.token import fetch_erc20_details
from eth_defi.uniswap_v2.constants import UNISWAP_V2_DEPLOYMENTS
from eth_defi.uniswap_v2.deployment import fetch_deployment
from eth_defi.uniswap_v3.constants import UNISWAP_V3_DEPLOYMENTS
from eth_defi.uniswap_v3.deployment import fetch_deployment as fetch_deployment_uni_v3
from eth_defi.aave_v3.deployment import fetch_deployment as fetch_aave_deployment
from tradeexecutor.cli.commands.shared_options import parse_comma_separated_list
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 lagoon_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_base: Optional[str] = shared_options.json_rpc_base,
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 data in this TXT file, paired with a JSON file."),
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"),
denomination_asset: Optional[str] = Option(..., envvar="DENOMINATION_ASSET", help="Stablecoin asset used for vault denomination"),
multisig_owners: Optional[str] = Option(None, callback=parse_comma_separated_list, envvar="MULTISIG_OWNERS", help="The list of acconts that are set to the cosigners of the Safe. The multisig threshold is number of cosigners - 1."),
# 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(None, envvar="WHITELISTED_ASSETS", help="Space separarted list of ERC-20 addresses this vault can trade. Denomination asset does not need to be whitelisted separately."),
any_asset: Optional[bool] = Option(False, envvar="ANY_ASSET", help="Allow trading of any ERC-20 on Uniswap (unsecure)."),
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."),
one_delta: bool = Option(False, envvar="ONE_DELTA", help="Whitelist 1delta interaction with GuardV0 smart contract."),
aave: bool = Option(False, envvar="AAVE", help="Whitelist Aave aUSDC deposits"),
uniswap_v2: bool = Option(False, envvar="UNISWAP_V2", help="Whitelist Uniswap v2"),
uniswap_v3: bool = Option(False, envvar="UNISWAP_V3", help="Whitelist Uniswap v3"),
verbose: bool = Option(False, envvar="VERBOSE", help="Extra verbosity with deploy commands"),
performance_fee: int = Option(DEFAULT_PERFORMANCE_RATE, envvar="PERFORMANCE_FEE", help="Performance fee in BPS"),
management_fee: int = Option(DEFAULT_MANAGEMENT_RATE, envvar="MANAGEMENT_FEE", help="Management fee in BPS"),
):
"""Deploy a new Lagoon vault.
Deploys a new Lagoon vault, Safe and TradingStrategyModuleV0 guard for automated trading.
TODO: Heavily under development.
"""
assert any_asset, "Currently only any_asset configurations supported"
assert private_key, "PRIVATE_KEY not set"
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_base=json_rpc_base,
json_rpc_anvil=json_rpc_anvil,
json_rpc_arbitrum=json_rpc_arbitrum,
simulate=simulate,
mev_endpoint_disabled=True,
)
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))
assert not whitelisted_assets, "whitelisted_assets: Not implemented"
whitelisted_asset_details = []
# 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:,}")
# TODO: Asset manager is now always the deployer
asset_manager = hot_wallet.address
denomination_token = fetch_erc20_details(
web3,
denomination_asset,
)
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("Fund: %s (%s)", fund_name, fund_symbol)
logger.info("Underlying token: %s", denomination_token.symbol)
logger.info("Whitelisting any token: %s", aave)
logger.info("Whitelisted assets: %s", ", ".join([a.symbol for a in whitelisted_asset_details]))
logger.info("Whitelisting Uniswap v2: %s", uniswap_v2)
logger.info("Whitelisting Uniswap v3: %s", uniswap_v3)
logger.info("Whitelisting 1delta: %s", one_delta)
logger.info("Whitelisting Aave: %s", aave)
logger.info("Multisig owners: %s", multisig_owners)
logger.info("Performance fee: %f %%", performance_fee / 100)
logger.info("Management fee: %f %%", management_fee / 100)
if etherscan_api_key:
logger.info("Etherscan API key: %s", etherscan_api_key)
else:
logger.error("Etherscan API key: not provided")
if asset_manager != hot_wallet.address:
logger.info("Asset manager is %s", asset_manager)
else:
logger.info("Hot wallet set for the asset manager role")
logger.info("-" * 80)
if not (simulate or unit_testing):
# TODO: Move this bit somewhere else
if not etherscan_api_key:
raise RuntimeError("Etherscan API key needed for production deployments")
confirm = input("Ok [y/n]? ")
if not confirm.lower().startswith("y"):
print("Aborted")
sys.exit(1)
# Currently assumes HotWallet = asset manager
# as the trade-executor that deploys the vault is going to
# the assset manager for this vault
parameters = LagoonDeploymentParameters(
underlying=denomination_token.address,
name=fund_name,
symbol=fund_symbol,
performanceRate=performance_fee,
managementRate=management_fee,
)
chain_slug = chain_id.get_slug()
if uniswap_v2:
uniswap_v2_deployment = fetch_deployment(
web3,
factory_address=UNISWAP_V2_DEPLOYMENTS[chain_slug]["factory"],
router_address=UNISWAP_V2_DEPLOYMENTS[chain_slug]["router"],
init_code_hash=UNISWAP_V2_DEPLOYMENTS[chain_slug]["init_code_hash"],
)
else:
uniswap_v2_deployment = None
if uniswap_v3:
chain_slug = chain_id.get_slug()
deployment_data = UNISWAP_V3_DEPLOYMENTS[chain_slug]
uniswap_v3_deployment= fetch_deployment_uni_v3(
web3,
factory_address=deployment_data["factory"],
router_address=deployment_data["router"],
position_manager_address=deployment_data["position_manager"],
quoter_address=deployment_data["quoter"],
quoter_v2=deployment_data["quoter_v2"],
router_v2=deployment_data["router_v2"],
)
else:
uniswap_v3_deployment = None
if aave:
chain_slug = chain_id.get_slug()
deployment_data = AAVE_V3_DEPLOYMENTS[chain_slug]
assert "ausdc" in deployment_data, f"No aUSDC configuration: {AAVE_V3_DEPLOYMENTS}"
aave_v3_deployment = fetch_aave_deployment(
web3,
pool_address=deployment_data["pool"],
data_provider_address=deployment_data["data_provider"],
oracle_address=deployment_data["oracle"],
ausdc_address=deployment_data["ausdc"],
)
else:
aave_v3_deployment = None
deploy_info = deploy_automated_lagoon_vault(
web3=web3,
deployer=hot_wallet,
asset_manager=asset_manager,
parameters=parameters,
safe_owners=multisig_owners,
safe_threshold=len(multisig_owners) - 1,
uniswap_v2=uniswap_v2_deployment,
uniswap_v3=uniswap_v3_deployment,
aave_v3=aave_v3_deployment,
any_asset=True,
use_forge=True,
etherscan_api_key=etherscan_api_key,
)
if vault_record_file and (not simulate):
# Make a small file, mostly used to communicate with unit tests
with open(vault_record_file, "wt") as out:
out.write(deploy_info.pformat())
#
with open(vault_record_file.with_suffix(".json"), "wt") as out:
out.write(json.dumps(deploy_info.get_deployment_data()))
logger.info("Wrote %s for vault details", os.path.abspath(vault_record_file))
else:
logger.info("Skipping record file because of simulation")
logger.info("Lagoon deployed:\n%s", deploy_info.pformat())
web3config.close()