From 01e7892230b5ec9502a59bc7282f03526274bfa7 Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Thu, 26 Sep 2024 21:34:29 +0200 Subject: [PATCH] Progress on Enzyme arbitrum deployment --- contracts/terms-of-service | 2 +- eth_defi/enzyme/generic_adapter_vault.py | 30 ++- eth_defi/foundry/forge.py | 8 +- tests/enzyme/test_arbitrum_trade.py | 313 +++++++---------------- 4 files changed, 124 insertions(+), 229 deletions(-) diff --git a/contracts/terms-of-service b/contracts/terms-of-service index ac990734..0df22e88 160000 --- a/contracts/terms-of-service +++ b/contracts/terms-of-service @@ -1 +1 @@ -Subproject commit ac9907344de7a29c864e5a7dc72ea75cd45f03d3 +Subproject commit 0df22e8898717fd68b00807ccd06e1f563eb00a1 diff --git a/eth_defi/enzyme/generic_adapter_vault.py b/eth_defi/enzyme/generic_adapter_vault.py index 9ab895af..f01deebf 100644 --- a/eth_defi/enzyme/generic_adapter_vault.py +++ b/eth_defi/enzyme/generic_adapter_vault.py @@ -24,7 +24,7 @@ from web3 import Web3 from web3.contract import Contract -from eth_defi.aave_v3.constants import AAVE_V3_DEPLOYMENTS +from eth_defi.aave_v3.constants import AAVE_V3_DEPLOYMENTS, AAVE_V3_NETWORKS from eth_defi.aave_v3.deployment import fetch_deployment as fetch_aave_deployment from eth_defi.enzyme.deployment import EnzymeDeployment from eth_defi.enzyme.policy import ( @@ -463,12 +463,30 @@ def deploy_guard( tx_hash = guard.functions.whitelistAaveV3(aave_pool_address, note).transact({"from": deployer.address}) assert_transaction_success_with_explanation(web3, tx_hash) - assert web3.eth.chain_id == 1, "TODO: Add support for non-mainnet chains" - ausdc_address = "0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c" - logger.info("Aave whitelisting for pool %s, aUSDC %s", aave_pool_address, ausdc_address) + match web3.eth.chain_id: + case 1: + assert web3.eth.chain_id == 1, "TODO: Add support for non-mainnet chains" + ausdc_address = "0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c" + logger.info("Aave whitelisting for pool %s, aUSDC %s", aave_pool_address, ausdc_address) + + note = f"Aave v3 pool whitelisting for USDC" + tx_hash = guard.functions.whitelistToken(ausdc_address, note).transact({"from": deployer.address}) + + case 42161: + # Arbitrum + aave_tokens = AAVE_V3_NETWORKS["arbitrum"].token_contracts + for symbol, token in aave_tokens.items(): + logger.info( + "Aave whitelisting for pool %s, atoken:%s address: %s", + aave_pool_address, + token.token_address, + ) + note = f"Whitelisting Aave {symbol}" + tx_hash = guard.functions.whitelistToken(token.token_address, note).transact({"from": deployer.address}) + assert_transaction_success_with_explanation(web3, tx_hash) + case _: + raise NotImplementedError(f"TODO: Add support for non-mainnet chains, got {web3.eth.chain_id}") - note = f"Aave v3 pool whitelisting for USDC" - tx_hash = guard.functions.whitelistToken(ausdc_address, note).transact({"from": deployer.address}) assert_transaction_success_with_explanation(web3, tx_hash) deployer.sync_nonce(web3) diff --git a/eth_defi/foundry/forge.py b/eth_defi/foundry/forge.py index e8449e52..81e200f7 100644 --- a/eth_defi/foundry/forge.py +++ b/eth_defi/foundry/forge.py @@ -191,6 +191,8 @@ def deploy_contract_with_forge( assert type(contract_name) == str assert isinstance(deployer, HotWallet), f"Got deployer: {type(deployer)}" + assert deployer.private_key is not None, f"Deployer missing private key: {deployer}" + if constructor_args is None: constructor_args = [] @@ -236,7 +238,11 @@ def deploy_contract_with_forge( for arg in constructor_args: cmd_line.append(arg) - censored_command = " ".join(cmd_line) + try: + censored_command = " ".join(cmd_line) + except TypeError as e: + # Be helpful with None error + raise TypeError(f"Could not splice command line: {cmd_line}") from e logger.info( "Deploying a contract with forge. Working directory %s, forge command: %s", diff --git a/tests/enzyme/test_arbitrum_trade.py b/tests/enzyme/test_arbitrum_trade.py index b9b2485b..32347206 100644 --- a/tests/enzyme/test_arbitrum_trade.py +++ b/tests/enzyme/test_arbitrum_trade.py @@ -2,30 +2,31 @@ - Use Arbitrum live RPC for testing -- Deploy a vault on a live mainnet fork +- Deploy a vault on a live mainnet fork and do a Uniswap v3 trade as an asset manager """ import os - -from eth_defi.enzyme.deployment import ARBITRUM_DEPLOYMENT -from eth_defi.provider.anvil import AnvilLaunch, launch_anvil - import datetime import random import pytest + from eth_account import Account from eth_account.signers.local import LocalAccount from eth_typing import HexAddress + +from web3 import Web3 +from web3.contract import Contract +from web3.middleware import construct_sign_and_send_raw_middleware + +from eth_defi.abi import get_contract +from eth_defi.enzyme.deployment import ARBITRUM_DEPLOYMENT +from eth_defi.provider.anvil import AnvilLaunch, launch_anvil from eth_defi.terms_of_service.acceptance_message import ( generate_acceptance_message, get_signing_hash, sign_terms_of_service, ) -from web3 import Web3 -from web3.contract import Contract -from web3.middleware import construct_sign_and_send_raw_middleware - from eth_defi.deploy import deploy_contract from eth_defi.enzyme.deployment import EnzymeDeployment, RateAsset from eth_defi.enzyme.generic_adapter_vault import deploy_vault_with_generic_adapter @@ -37,8 +38,9 @@ from eth_defi.trace import ( assert_transaction_success_with_explanation, ) +from eth_defi.uniswap_v3.constants import UNISWAP_V3_DEPLOYMENTS from eth_defi.uniswap_v3.deployment import ( - UniswapV3Deployment, + UniswapV3Deployment, fetch_deployment, ) from eth_defi.uniswap_v3.pool import PoolDetails, fetch_pool_details @@ -56,12 +58,10 @@ def usdt_whale() -> HexAddress: @pytest.fixture() def anvil(usdt_whale) -> AnvilLaunch: """Launch Polygon fork.""" - rpc_url = os.environ["JSON_RPC_POLYGON"] anvil = launch_anvil( fork_url=JSON_RPC_ARBITRUM, unlocked_addresses=[usdt_whale], - fork_block_number=57_000_000, ) try: yield anvil @@ -70,24 +70,25 @@ def anvil(usdt_whale) -> AnvilLaunch: @pytest.fixture -def deployer(web3, deployer) -> Account: +def deployer(web3) -> Account: return web3.eth.accounts[0] @pytest.fixture -def vault_owner(web3, deployer) -> Account: +def vault_owner(web3) -> Account: return web3.eth.accounts[1] @pytest.fixture -def asset_manager(web3, deployer) -> Account: - """Create a LocalAccount user. - - See limitations in `transfer_with_authorization`. - """ +def asset_manager(web3) -> Account: return web3.eth.accounts[2] +@pytest.fixture +def user_1(web3) -> Account: + return web3.eth.accounts[3] + + @pytest.fixture def usdt(web3) -> TokenDetails: details = fetch_erc20_details(web3, "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9") @@ -95,84 +96,44 @@ def usdt(web3) -> TokenDetails: @pytest.fixture -def vault_investor(web3, deployer, usdt: TokenDetails) -> LocalAccount: - """Create a LocalAccount user. - - See limitations in `transfer_with_authorization`. - """ - account = Account.create() - stash = web3.eth.get_balance(deployer) - tx_hash = web3.eth.send_transaction({"from": deployer, "to": account.address, "value": stash // 2}) - assert_transaction_success_with_explanation(web3, tx_hash) - usdt.functions.transfer( - account.address, - 500 * 10**6, - ).transact({"from": deployer}) - web3.middleware_onion.add(construct_sign_and_send_raw_middleware_anvil(account)) - return account - - -@pytest.fixture() -def acceptance_message(web3: Web3) -> str: - """The message user needs to sign in order to deposit.""" - - # Generate the message user needs to sign in their wallet - signing_content = generate_acceptance_message( - 1, - datetime.datetime.utcnow(), - "https://example.com/terms-of-service", - random.randbytes(32), - ) - - return signing_content - - -@pytest.fixture() -def terms_of_service( - web3: Web3, - deployer: str, - acceptance_message: str, -) -> Contract: - """Deploy Terms of Service contract.""" +def weth(web3) -> TokenDetails: + details = fetch_erc20_details(web3, "0xec32aad0e8fc6851f4ba024b33de09607190ce9b") + return details - tos = deploy_contract( - web3, - "terms-of-service/TermsOfService.json", - deployer, - ) - new_version = 1 - new_hash = get_signing_hash(acceptance_message) - tx_hash = tos.functions.updateTermsOfService(new_version, new_hash).transact({"from": deployer}) - assert_transaction_success_with_explanation(web3, tx_hash) - return tos +@pytest.fixture +def wbtc(web3) -> TokenDetails: + details = fetch_erc20_details(web3, "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f") + return details @pytest.fixture() def enzyme( web3, - deployer, - mln, - weth, - usdc, - usdc_usd_mock_chainlink_aggregator, - mln_usd_mock_chainlink_aggregator, - weth_usd_mock_chainlink_aggregator, ) -> EnzymeDeployment: """Deploy Enzyme protocol with few Chainlink feeds mocked with a static price.""" deployment = EnzymeDeployment.fetch_deployment(web3, ARBITRUM_DEPLOYMENT) return deployment +@pytest.fixture() +def terms_of_service(web3) -> Contract: + tos = get_contract( + web3, + "terms-of-service/TermsOfService.json", + ) + return tos + + @pytest.fixture() def vault( web3: Web3, deployer: HexAddress, asset_manager: HexAddress, enzyme: EnzymeDeployment, - weth: Contract, - mln: Contract, - usdc: Contract, + weth: TokenDetails, + wbtc: TokenDetails, + usdt: TokenDetails, terms_of_service: Contract, ) -> Vault: """Deploy an Enzyme vault. @@ -185,110 +146,97 @@ def vault( - TermedVaultUSDCPaymentForwarder """ - _deployer = web3.eth.accounts[0] - account: LocalAccount = Account.create() - stash = web3.eth.get_balance(_deployer) - tx_hash = web3.eth.send_transaction({"from": _deployer, "to": account.address, "value": stash // 2}) + # Note that the only way to deploy vault is with a local private key, + # because we call external foundry processes + local_signer: LocalAccount = Account.create() + stash = web3.eth.get_balance(deployer) + tx_hash = web3.eth.send_transaction({"from": deployer, "to": local_signer.address, "value": stash // 2}) assert_transaction_success_with_explanation(web3, tx_hash) - hot_wallet = HotWallet(account) + hot_wallet = HotWallet(local_signer) hot_wallet.sync_nonce(web3) - web3.middleware_onion.add(construct_sign_and_send_raw_middleware(account)) - return deploy_vault_with_generic_adapter(enzyme, hot_wallet, asset_manager, deployer, usdc, terms_of_service) + # TODO: Hack + enzyme.deployer = hot_wallet.address + web3.middleware_onion.add(construct_sign_and_send_raw_middleware_anvil(local_signer)) -@pytest.fixture() -def payment_forwarder(vault: Vault) -> Contract: - return vault.payment_forwarder + return deploy_vault_with_generic_adapter( + enzyme, + deployer=hot_wallet, + asset_manager=asset_manager, + owner=deployer, + denomination_asset=usdt.contract, + terms_of_service=terms_of_service, + whitelisted_assets=[weth, wbtc, usdt], + uniswap_v3=True, + one_delta=False, + aave=True, + ) @pytest.fixture() def uniswap( web3: Web3, - vault: Vault, weth: Contract, usdc: Contract, mln: Contract, deployer: str, ) -> UniswapV3Deployment: - """Deploy Uniswap v3.""" - assert web3.eth.get_balance(deployer) > 0 - uniswap = deploy_uniswap_v3(web3, vault.deployer_hot_wallet.address, weth=weth, give_weth=500) + addresses = UNISWAP_V3_DEPLOYMENTS["arbitrum"] + uniswap = fetch_deployment( + web3, + addresses["factory"], + addresses["router"], + addresses["position_manager"], + addresses["quoter_address"], + ) return uniswap -@pytest.fixture() -def guard(vault, uniswap) -> Contract: - guard = vault.guard_contract - guard.functions.whitelistUniswapV3Router(uniswap.swap_router.address, "").transact({"from": vault.deployer_hot_wallet.address}) - guard.functions.whitelistToken(usdt.address, "").transact({"from": vault.deployer_hot_wallet.address}) - guard.functions.whitelistToken(weth.address, "").transact({"from": vault.deployer_hot_wallet.address}) - guard.functions.whitelistToken(mln.address, "").transact({"from": vault.deployer_hot_wallet.address}) - return guard - - @pytest.fixture() def weth_usdt_pool(web3) -> PoolDetails: - """Mock WETH-USDT pool.""" # https://tradingstrategy.ai/trading-view/arbitrum/uniswap-v3/eth-usdt-fee-5 return fetch_pool_details(web3, "0x641c00a822e8b671738d32a431a4fb6074e5c79d") -def test_enzyme_guarded_trade_singlehop_uniswap_v3( +def test_enzyme_uniswap_v3_arbitrum( web3: Web3, deployer: HexAddress, asset_manager: HexAddress, + user_1, + usdt_whale, enzyme: EnzymeDeployment, vault: Vault, vault_investor: LocalAccount, - weth_token: TokenDetails, - mln: Contract, - usdc_token: TokenDetails, - usdc_usd_mock_chainlink_aggregator: Contract, - payment_forwarder: Contract, - acceptance_message: str, - terms_of_service: Contract, + weth: TokenDetails, + usdt: TokenDetails, + wbtc: TokenDetails, uniswap_v3: UniswapV3Deployment, - weth_usdc_pool: PoolDetails, + weth_usdt_pool: PoolDetails, ): """Make a swap that goes through the call guard.""" - assert vault.is_supported_asset(usdc_token.address) - assert vault.is_supported_asset(weth_token.address) - - # Sign terms of service - acceptance_hash, signature = sign_terms_of_service(vault_investor, acceptance_message) - - # The transfer will expire in one hour - # in the test EVM timeline - block = web3.eth.get_block("latest") - valid_before = block["timestamp"] + 3600 - - # Construct bounded ContractFunction instance - # that will transact with MockEIP3009Receiver.deposit() - # smart contract function. - bound_func = make_eip_3009_transfer( - token=usdc_token, - from_=vault_investor, - to=payment_forwarder.address, - func=payment_forwarder.functions.buySharesOnBehalfUsingTransferWithAuthorizationAndTermsOfService, - value=500 * 10**6, # 500 USD, - valid_before=valid_before, - extra_args=(1, acceptance_hash, signature), - authorization_type=EIP3009AuthorizationType.TransferWithAuthorization, - ) + assert vault.is_supported_asset(usdt.address) + assert vault.is_supported_asset(weth.address) + assert vault.is_supported_asset(wbtc.address) - # Sign and broadcast the tx - tx_hash = bound_func.transact({"from": vault_investor.address}) + tx_hash = usdt.contract.functions.transfer( + user_1, + 500 * 10 ** 6, + ).transact({"from": usdt_whale}) + assert_transaction_success_with_explanation(web3, tx_hash) - # Print out Solidity stack trace if this fails + tx_hash = usdt.contract.functions.transfer(user_1, 500 * 10 ** 6).transact({"from": deployer}) assert_transaction_success_with_explanation(web3, tx_hash) - assert payment_forwarder.functions.amountProxied().call() == 500 * 10**6 # Got shares + tx_hash = vault.comptroller.functions.buyShares(500 * 10 ** 6, 1).transact({"from": user_1}) + assert_transaction_success_with_explanation(web3, tx_hash) assert vault.get_gross_asset_value() == 500 * 10**6 # Vault has been funded + pool_fee_raw = 500 # 5 BPS + # Vault swaps USDC->ETH for both users # Buy ETH worth of 200 USD prepared_tx = prepare_swap( @@ -296,9 +244,9 @@ def test_enzyme_guarded_trade_singlehop_uniswap_v3( vault, uniswap_v3, vault.generic_adapter, - token_in=usdc_token.contract, - token_out=weth_token.contract, - pool_fees=[POOL_FEE_RAW], + token_in=usdt.contract, + token_out=weth.contract, + pool_fees=[pool_fee_raw], token_in_amount=200 * 10**6, # 200 USD ) @@ -306,81 +254,4 @@ def test_enzyme_guarded_trade_singlehop_uniswap_v3( assert_transaction_success_with_explanation(web3, tx_hash) # Bought ETH landed in the vault - assert weth_token.contract.functions.balanceOf(vault.address).call() == pytest.approx(0.123090978678222650 * 10**18) - - -def test_enzyme_guarded_trade_multihops_uniswap_v3( - web3: Web3, - deployer: HexAddress, - asset_manager: HexAddress, - enzyme: EnzymeDeployment, - vault: Vault, - vault_investor: LocalAccount, - weth_token: TokenDetails, - mln_token: TokenDetails, - usdc_token: TokenDetails, - usdc_usd_mock_chainlink_aggregator: Contract, - payment_forwarder: Contract, - acceptance_message: str, - terms_of_service: Contract, - uniswap_v3: UniswapV3Deployment, - weth_usdc_pool: PoolDetails, - weth_mln_pool: PoolDetails, -): - """Make a swap that goes through the call guard.""" - - assert vault.is_supported_asset(usdc_token.address) - assert vault.is_supported_asset(weth_token.address) - assert vault.is_supported_asset(mln_token.address) - - # Sign terms of service - acceptance_hash, signature = sign_terms_of_service(vault_investor, acceptance_message) - - # The transfer will expire in one hour - # in the test EVM timeline - block = web3.eth.get_block("latest") - valid_before = block["timestamp"] + 3600 - - # Construct bounded ContractFunction instance - # that will transact with MockEIP3009Receiver.deposit() - # smart contract function. - bound_func = make_eip_3009_transfer( - token=usdc_token, - from_=vault_investor, - to=payment_forwarder.address, - func=payment_forwarder.functions.buySharesOnBehalfUsingTransferWithAuthorizationAndTermsOfService, - value=500 * 10**6, # 500 USD, - valid_before=valid_before, - extra_args=(1, acceptance_hash, signature), - authorization_type=EIP3009AuthorizationType.TransferWithAuthorization, - ) - - # Sign and broadcast the tx - tx_hash = bound_func.transact({"from": vault_investor.address}) - - # Print out Solidity stack trace if this fails - assert_transaction_success_with_explanation(web3, tx_hash) - - assert payment_forwarder.functions.amountProxied().call() == 500 * 10**6 # Got shares - - assert vault.get_gross_asset_value() == 500 * 10**6 # Vault has been funded - - # Vault swaps USDC->ETH->MLN for both users - # Buy MLN worth of 200 USD - prepared_tx = prepare_swap( - enzyme, - vault, - uniswap_v3, - vault.generic_adapter, - token_in=usdc_token.contract, - token_out=mln_token.contract, - token_intermediate=weth_token.contract, - pool_fees=[POOL_FEE_RAW, POOL_FEE_RAW], - token_in_amount=200 * 10**6, # 200 USD - ) - - tx_hash = prepared_tx.transact({"from": asset_manager, "gas": 1_000_000}) - assert_transaction_success_with_explanation(web3, tx_hash) - - # Bought MLN landed in the vault - assert mln_token.contract.functions.balanceOf(vault.address).call() == pytest.approx(0.969871220879840482 * 10**18) + assert weth.contract.functions.balanceOf(vault.address).call() == pytest.approx(0.123090978678222650 * 10**18)