From fdc419c3f70cdeba3076fdc13a574effb6a892cc Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Wed, 7 Jun 2023 16:55:53 -0500 Subject: [PATCH 01/13] wip --- README.md | 7 + requirements.txt | 2 +- .../abis/aave_variable_debt_token.abi.json | 515 ++++++++++++++++++ ui_workflows/aave/common.py | 5 + .../aave_borrow_contract_workflow.py | 102 ++++ .../tests/test_aave_borrow_erc20.py | 47 ++ .../tests/test_aave_borrow_eth_no_approval.py | 54 ++ .../test_aave_borrow_eth_with_approval.py | 57 ++ ui_workflows/conftest.py | 5 +- 9 files changed, 791 insertions(+), 3 deletions(-) create mode 100644 ui_workflows/aave/abis/aave_variable_debt_token.abi.json create mode 100644 ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py diff --git a/README.md b/README.md index 33ac8e23..44df32c8 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,10 @@ you are modifying weaviate schema. cd docker docker-compose up ``` + + +# DRAFT +- the Weaviate vector index may not have been updated with the latest widgets.txt changes, so anytime the file is changed the following needs to be done to allow semantic search to query against the latest state of the widgets:- + - Bump up the index version on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py#L9 (so use `WidgetV11` as v10 already taken by pending PR) + - Similarly, bump up the index version on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/config.py#L6 + - Finally, run this command on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py (`python3 -c "from index import widgets; widgets.backfill()"`) diff --git a/requirements.txt b/requirements.txt index 2ad4ca60..057ebf99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ SQLAlchemy_Utils==0.39.0 psycopg2-binary==2.9.5 qcore==1.8.0 PyYAML==6.0 -web3==6.2.0 +web3==6.3.0 gpt_index==0.4.23 transformers==4.26.1 pyWalletConnect==1.3.3 diff --git a/ui_workflows/aave/abis/aave_variable_debt_token.abi.json b/ui_workflows/aave/abis/aave_variable_debt_token.abi.json new file mode 100644 index 00000000..2907474e --- /dev/null +++ b/ui_workflows/aave/abis/aave_variable_debt_token.abi.json @@ -0,0 +1,515 @@ +[ + { + "inputs": [ + { "internalType": "contract IPool", "name": "pool", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "fromUser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "toUser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "BorrowAllowanceDelegated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "incentivesController", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "debtTokenDecimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "string", + "name": "debtTokenName", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "debtTokenSymbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DEBT_TOKEN_REVISION", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DELEGATION_WITH_SIG_TYPEHASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EIP712_REVISION", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "POOL", + "outputs": [ + { "internalType": "contract IPool", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNDERLYING_ASSET_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "delegatee", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approveDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "fromUser", "type": "address" }, + { "internalType": "address", "name": "toUser", "type": "address" } + ], + "name": "borrowAllowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "burn", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "delegator", "type": "address" }, + { "internalType": "address", "name": "delegatee", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "delegationWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getIncentivesController", + "outputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getPreviousIndex", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getScaledUserBalanceAndSupply", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "initializingPool", + "type": "address" + }, + { + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "internalType": "contract IAaveIncentivesController", + "name": "incentivesController", + "type": "address" + }, + { "internalType": "uint8", "name": "debtTokenDecimals", "type": "uint8" }, + { "internalType": "string", "name": "debtTokenName", "type": "string" }, + { "internalType": "string", "name": "debtTokenSymbol", "type": "string" }, + { "internalType": "bytes", "name": "params", "type": "bytes" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "mint", + "outputs": [ + { "internalType": "bool", "name": "", "type": "bool" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "scaledBalanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "scaledTotalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "controller", + "type": "address" + } + ], + "name": "setIncentivesController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/ui_workflows/aave/common.py b/ui_workflows/aave/common.py index 8e315f36..0b36206b 100644 --- a/ui_workflows/aave/common.py +++ b/ui_workflows/aave/common.py @@ -12,6 +12,7 @@ FIVE_SECONDS = 5000 AAVE_POOL_V3_PROXY_ADDRESS = Web3.to_checksum_address("0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2") AAVE_WRAPPED_TOKEN_GATEWAY = Web3.to_checksum_address("0xd322a49006fc828f9b5b37ab215f99b4e5cab19c") +AAVE_VARIABLE_DEBT_TOKEN_ADDRESS = Web3.to_checksum_address("0xea51d7853eefb32b6ee06b1c12e6dcca88be0ffe") AAVE_SUPPORTED_TOKENS = [ "ETH", @@ -35,6 +36,10 @@ def get_aave_wrapped_token_gateway_contract(): web3_provider = context.get_web3_provider() return web3_provider.eth.contract(address=AAVE_WRAPPED_TOKEN_GATEWAY, abi=load_contract_abi(__file__, "./abis/aave_wrapped_token_gateway.abi.json")) +def get_aave_variable_debt_token_contract(): + web3_provider = context.get_web3_provider() + return web3_provider.eth.contract(address=AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, abi=load_contract_abi(__file__, "./abis/aave_variable_debt_token.abi.json")) + class AaveMixin: def _goto_page_and_open_walletconnect(self, page): """Go to page and open WalletConnect modal""" diff --git a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py new file mode 100644 index 00000000..469a1aa2 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py @@ -0,0 +1,102 @@ +from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict + +import web3 + +from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult, tenderly_simulate_tx +from database.models import ( + db_session, MultiStepWorkflow, WorkflowStep, WorkflowStepStatus, WorkflowStepUserActionType, ChatMessage, ChatSession, SystemConfig +) +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract + +class AaveBorrowContractWorkflow(BaseMultiStepContractWorkflow): + """ + NOTE: Refer to the docstring in ../ui_integration/aave_borrow_ui_workflow.py (AaveBorrowUIWorkflow) to get more info on the various scenarios to handle for Aave borrow + """ + + WORKFLOW_TYPE = 'aave-borrow' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + self.token = workflow_params["token"] + self.amount = workflow_params["amount"] + + if self.token == "ETH": + # check_ETH_liquidation_risk_step = RunnableStep("check_ETH_liquidation_risk", WorkflowStepUserActionType.acknowledge, f"Acknowledge liquidation risk due to high borrow amount of {self.amount} ETH on Aave", self.check_ETH_liquidation_risk) + initiate_ETH_approval_step = RunnableStep("initiate_ETH_approval", WorkflowStepUserActionType.tx, f"Approve borrow of {self.amount} ETH on Aave", self.initiate_ETH_approval) + confirm_ETH_borrow_step = RunnableStep("confirm_ETH_borrow", WorkflowStepUserActionType.tx, f"Confirm borrow of {self.amount} ETH on Aave", self.confirm_ETH_borrow) + steps = [initiate_ETH_approval_step, confirm_ETH_borrow_step] + + final_step_type = "confirm_ETH_borrow" + else: + # check_ERC20_liquidation_risk = RunnableStep("check_ERC20_liquidation_risk", WorkflowStepUserActionType.acknowledge, f"Acknowledge liquidation risk due to high borrow amount of {self.amount} {self.token} on Aave", self.check_ERC20_liquidation_risk) + confirm_ERC20_borrow_step = RunnableStep("confirm_ERC20_borrow", WorkflowStepUserActionType.tx, f"Confirm borrow of {self.amount} {self.token} on Aave", self.confirm_ERC20_borrow) + steps = [confirm_ERC20_borrow_step] + final_step_type = "confirm_ERC20_borrow" + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + if (self.token not in AAVE_SUPPORTED_TOKENS): + raise WorkflowValidationError(f"Token {self.token} not supported by Aave") + + # TODO: add a check to verify that the user has deposited collateral + + + def check_ETH_liquidation_risk(self): + # TODO: add a check to get the health factor and ensure that it is not too high + pass + + def initiate_ETH_approval(self): + """Initiate approval for ETH token""" + + from_address = self.wallet_address + to_address = AAVE_WRAPPED_TOKEN_GATEWAY + + borrow_allowance = get_aave_variable_debt_token_contract().functions.borrowAllowance(from_address, to_address).call() + + if parse_token_amount(self.wallet_chain_id, self.token, self.amount) <= borrow_allowance: + return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ETH_borrow") + + delegatee = AAVE_WRAPPED_TOKEN_GATEWAY + amount = int(web3.constants.MAX_INT, 16) + encoded_data = get_aave_variable_debt_token_contract().encodeABI(fn_name='approveDelegation', args=[delegatee, amount]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) + + def confirm_ETH_borrow(self, extra_params=None): + pool_address = AAVE_POOL_V3_PROXY_ADDRESS + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 # Variable + referral_code = 0 + encoded_data = get_aave_wrapped_token_gateway_contract().encodeABI(fn_name='borrowETH', args=[pool_address, amount, interest_rate_mode, referral_code]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_WRAPPED_TOKEN_GATEWAY, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) + + def check_ERC20_liquidation_risk(self, page, browser_context): + pass + + def confirm_ERC20_borrow(self): + asset_address = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 # Variable + referral_code = 0 + on_behalf_of = self.wallet_address + + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='borrow', args=[asset_address, amount, interest_rate_mode, referral_code, on_behalf_of]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_POOL_V3_PROXY_ADDRESS, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py new file mode 100644 index 00000000..8c08fc3f --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py @@ -0,0 +1,47 @@ + +""" +Test for borrowing ETH on Aave +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_erc20" +def test_contract_aave_borrow_erc20(setup_fork): + token = "DAI" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Confirm borrow of 0.1 DAI on Aave" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py new file mode 100644 index 00000000..d517ca43 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py @@ -0,0 +1,54 @@ + +""" +Test for borrowing ETH on Aave +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_eth_no_approval" +def test_contract_aave_borrow_eth_no_approval(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + # Pre-approve ETH borrow to set the test environment for borrow + aave_set_eth_approval(1*10**18) + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + assert multistep_result.description == "Confirm borrow of 0.1 ETH on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + # TODO - For thorough validation, figure out how to fetch decoded tx data from Tenderly and assert the amount processed diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py new file mode 100644 index 00000000..3359f290 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py @@ -0,0 +1,57 @@ + +""" +Test for borrowing ETH on Aave +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_eth_with_approval" +def test_contract_aave_borrow_eth_with_approval(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + assert multistep_result.description == "Approve borrow of 0.1 ETH on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + assert multistep_result.description == "Confirm borrow of 0.1 ETH on Aave" + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + # TODO - For thorough validation, figure out how to fetch decoded tx data from Tenderly and assert the amount processed diff --git a/ui_workflows/conftest.py b/ui_workflows/conftest.py index 72960edb..e4984608 100644 --- a/ui_workflows/conftest.py +++ b/ui_workflows/conftest.py @@ -10,11 +10,12 @@ @pytest.fixture(scope="module") def setup_fork(): # Before test - fork_id = create_fork() + #fork_id = create_fork() + fork_id = "da6416f8-c838-4c8c-8215-47d2710df1ee" with context.with_request_context(None, None, wallet_chain_id=None, fork_id=fork_id): # Return to test function yield {"fork_id": fork_id} # After test - remove_fork(fork_id) \ No newline at end of file + #remove_fork(fork_id) \ No newline at end of file From 7eb80538f1ac63740b60f031552a615994902d57 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Thu, 8 Jun 2023 13:50:44 -0500 Subject: [PATCH 02/13] wip --- ui_workflows/conftest.py | 8 ++++---- utils/constants.py | 4 +++- utils/tenderly.py | 5 ++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ui_workflows/conftest.py b/ui_workflows/conftest.py index e4984608..3fbd08f7 100644 --- a/ui_workflows/conftest.py +++ b/ui_workflows/conftest.py @@ -5,17 +5,17 @@ import pytest import context -from utils import create_fork, remove_fork +from utils import create_fork, remove_fork, TEST_TENDERLY_FORK_ID @pytest.fixture(scope="module") def setup_fork(): # Before test - #fork_id = create_fork() - fork_id = "da6416f8-c838-4c8c-8215-47d2710df1ee" + fork_id = TEST_TENDERLY_FORK_ID or create_fork() with context.with_request_context(None, None, wallet_chain_id=None, fork_id=fork_id): # Return to test function yield {"fork_id": fork_id} # After test - #remove_fork(fork_id) \ No newline at end of file + if not TEST_TENDERLY_FORK_ID: + remove_fork(fork_id) \ No newline at end of file diff --git a/utils/constants.py b/utils/constants.py index 8e2be514..051f1e3f 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -8,4 +8,6 @@ TENDERLY_FORK_BASE_URL = "https://rpc.tenderly.co/fork" DEFAULT_MAINNET_FORK_ID = "08f78838-4799-47a8-88fb-1f169fa99f57" -TENDERLY_FORK_URL = f"{TENDERLY_FORK_BASE_URL}/{DEFAULT_MAINNET_FORK_ID}" \ No newline at end of file +TENDERLY_FORK_URL = f"{TENDERLY_FORK_BASE_URL}/{DEFAULT_MAINNET_FORK_ID}" + +TEST_TENDERLY_FORK_ID = os.getenv('TEST_TENDERLY_FORK_ID', None) \ No newline at end of file diff --git a/utils/tenderly.py b/utils/tenderly.py index d6708a87..389affb7 100644 --- a/utils/tenderly.py +++ b/utils/tenderly.py @@ -8,6 +8,8 @@ def create_fork(): if not TENDERLY_API_KEY: raise Exception("TENDERLY_API_KEY required to run simulations in isolated forks") + print("Creating fork...") + payload = { "network_id": "1", "block_number": 17297193 # https://etherscan.io/block/17297193 @@ -18,4 +20,5 @@ def create_fork(): def remove_fork(fork_id: str): res = requests.delete(f"{TENDERLY_PROJECT_URL}/{fork_id}", headers={"X-Access-Key": TENDERLY_API_KEY}) - res.raise_for_status() \ No newline at end of file + res.raise_for_status() + print(f"Fork deleted: {fork_id}") \ No newline at end of file From 8b51945c3d5942fe65cba1f85d5a43a7cafc6665 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Thu, 8 Jun 2023 16:48:09 -0500 Subject: [PATCH 03/13] wip --- ui_workflows/aave/common.py | 6 +- .../aave_borrow_contract_workflow.py | 5 +- .../aave_repay_contract_workflow.py | 78 +++++++++++++++++++ .../tests/test_aave_repay_erc20.py | 66 ++++++++++++++++ .../tests/test_aave_repay_eth.py | 56 +++++++++++++ utils/constants.py | 2 +- 6 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py diff --git a/ui_workflows/aave/common.py b/ui_workflows/aave/common.py index 0b36206b..e25c0fe4 100644 --- a/ui_workflows/aave/common.py +++ b/ui_workflows/aave/common.py @@ -7,7 +7,7 @@ from web3 import Web3 from utils import load_contract_abi -from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS +from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError FIVE_SECONDS = 5000 AAVE_POOL_V3_PROXY_ADDRESS = Web3.to_checksum_address("0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2") @@ -40,6 +40,10 @@ def get_aave_variable_debt_token_contract(): web3_provider = context.get_web3_provider() return web3_provider.eth.contract(address=AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, abi=load_contract_abi(__file__, "./abis/aave_variable_debt_token.abi.json")) +def common_aave_validation(token): + if (token not in AAVE_SUPPORTED_TOKENS): + raise WorkflowValidationError(f"Token {token} not supported by Aave") + class AaveMixin: def _goto_page_and_open_walletconnect(self, page): """Go to page and open WalletConnect modal""" diff --git a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py index 469a1aa2..da31c8f6 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py @@ -7,7 +7,7 @@ from database.models import ( db_session, MultiStepWorkflow, WorkflowStep, WorkflowStepStatus, WorkflowStepUserActionType, ChatMessage, ChatSession, SystemConfig ) -from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract, common_aave_validation class AaveBorrowContractWorkflow(BaseMultiStepContractWorkflow): """ @@ -36,8 +36,7 @@ def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: s super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) def _general_workflow_validation(self): - if (self.token not in AAVE_SUPPORTED_TOKENS): - raise WorkflowValidationError(f"Token {self.token} not supported by Aave") + common_aave_validation(self.token) # TODO: add a check to verify that the user has deposited collateral diff --git a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py new file mode 100644 index 00000000..cb1f96eb --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py @@ -0,0 +1,78 @@ +from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict + +import web3 + +from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult, tenderly_simulate_tx +from database.models import ( + MultiStepWorkflow, WorkflowStepUserActionType +) +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract, common_aave_validation + +class AaveRepayContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'aave-repay' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + self.token = workflow_params["token"] + self.amount = workflow_params["amount"] + + if self.token == "ETH": + confirm_ETH_repay_step = RunnableStep("confirm_ETH_repay", WorkflowStepUserActionType.tx, f"Confirm repay of {self.amount} ETH on Aave", self.confirm_ETH_repay) + steps = [confirm_ETH_repay_step] + + final_step_type = confirm_ETH_repay_step.type + else: + initiate_ERC20_approval_step = RunnableStep("initiate_ERC20_approve", WorkflowStepUserActionType.tx, f"Approve repay of {self.amount} {self.token} on Aave", self.initiate_ERC20_approval) + confirm_ERC20_repay_step = RunnableStep("confirm_ERC20_repay", WorkflowStepUserActionType.tx, f"Confirm repay of {self.amount} {self.token} on Aave", self.confirm_ERC20_repay) + steps = [initiate_ERC20_approval_step, confirm_ERC20_repay_step] + final_step_type = confirm_ERC20_repay_step.type + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + common_aave_validation(self.token) + + def confirm_ETH_repay(self): + pool_address = AAVE_POOL_V3_PROXY_ADDRESS + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 + on_behalf_of = self.wallet_address + encoded_data = get_aave_wrapped_token_gateway_contract().encodeABI(fn_name='repayETH', args=[pool_address, amount, interest_rate_mode, on_behalf_of]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_WRAPPED_TOKEN_GATEWAY, + 'data': encoded_data, + 'value': hexify_token_amount(self.wallet_chain_id, self.token, self.amount) + } + + return ContractStepProcessingResult(status="success", tx=tx) + + + def initiate_ERC20_approval(self): + if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address, AAVE_POOL_V3_PROXY_ADDRESS, self.amount)): + return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ERC20_repay") + + spender = AAVE_POOL_V3_PROXY_ADDRESS + value = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + encoded_data = generate_erc20_approve_encoded_data(self.web3_provider, self.wallet_chain_id, self.token, spender, value) + tx = { + 'from': self.wallet_address, + 'to': get_token_address(self.wallet_chain_id, self.token), + 'data': encoded_data, + } + return ContractStepProcessingResult(status="success", tx=tx) + + def confirm_ERC20_repay(self): + asset_address = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 + on_behalf_of = self.wallet_address + + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='repay', args=[asset_address, amount, interest_rate_mode, on_behalf_of]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_POOL_V3_PROXY_ADDRESS, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py new file mode 100644 index 00000000..24214e45 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py @@ -0,0 +1,66 @@ + +""" +Test for Repaying ETH on Aave +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_repay_contract_workflow import AaveRepayContractWorkflow +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_repay_erc20" +def test_contract_aave_repay_erc20(setup_fork): + token = "USDT" + amount = 10 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + # First borrow in order to test repay + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Approve repay of 10 USDT on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + assert multistep_result.description == "Confirm repay of 10 USDT on Aave" + + assert multistep_result.is_final_step == True + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py new file mode 100644 index 00000000..b93dd52d --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py @@ -0,0 +1,56 @@ + +""" +Test for Repaying ETH on Aave +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_repay_contract_workflow import AaveRepayContractWorkflow +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_repay_eth" +def test_contract_aave_repay_eth(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + # Pre-approve ETH borrow to set the test environment for borrow + aave_set_eth_approval(1*10**18) + + # First borrow in order to test repay + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Confirm repay of 0.1 ETH on Aave" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file diff --git a/utils/constants.py b/utils/constants.py index 051f1e3f..1c65f2e9 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -10,4 +10,4 @@ DEFAULT_MAINNET_FORK_ID = "08f78838-4799-47a8-88fb-1f169fa99f57" TENDERLY_FORK_URL = f"{TENDERLY_FORK_BASE_URL}/{DEFAULT_MAINNET_FORK_ID}" -TEST_TENDERLY_FORK_ID = os.getenv('TEST_TENDERLY_FORK_ID', None) \ No newline at end of file +TEST_TENDERLY_FORK_ID = os.getenv('TEST_TENDERLY_FORK_ID', "") \ No newline at end of file From f6ac80d0814d8656d7b1a35b73ab739f67c29e47 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Thu, 8 Jun 2023 17:53:43 -0500 Subject: [PATCH 04/13] wip --- ui_workflows/aave/abis/aave_atoken.abi.json | 568 ++++++++++++++++++ ui_workflows/aave/common.py | 5 + .../aave_repay_contract_workflow.py | 1 - .../aave_withdraw_contract_workflow.py | 72 +++ .../tests/test_aave_repay_erc20.py | 3 - .../tests/test_aave_withdraw_erc20.py | 48 ++ .../tests/test_aave_withdraw_eth.py | 56 ++ 7 files changed, 749 insertions(+), 4 deletions(-) create mode 100644 ui_workflows/aave/abis/aave_atoken.abi.json create mode 100644 ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py diff --git a/ui_workflows/aave/abis/aave_atoken.abi.json b/ui_workflows/aave/abis/aave_atoken.abi.json new file mode 100644 index 00000000..79bd31f9 --- /dev/null +++ b/ui_workflows/aave/abis/aave_atoken.abi.json @@ -0,0 +1,568 @@ +[ + { + "inputs": [ + { "internalType": "contract IPool", "name": "pool", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "BalanceTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "treasury", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "incentivesController", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "aTokenDecimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "string", + "name": "aTokenName", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "aTokenSymbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "ATOKEN_REVISION", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EIP712_REVISION", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "POOL", + "outputs": [ + { "internalType": "contract IPool", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RESERVE_TREASURY_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNDERLYING_ASSET_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { + "internalType": "address", + "name": "receiverOfUnderlying", + "type": "address" + }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getIncentivesController", + "outputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getPreviousIndex", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getScaledUserBalanceAndSupply", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "handleRepayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "initializingPool", + "type": "address" + }, + { "internalType": "address", "name": "treasury", "type": "address" }, + { + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "internalType": "contract IAaveIncentivesController", + "name": "incentivesController", + "type": "address" + }, + { "internalType": "uint8", "name": "aTokenDecimals", "type": "uint8" }, + { "internalType": "string", "name": "aTokenName", "type": "string" }, + { "internalType": "string", "name": "aTokenSymbol", "type": "string" }, + { "internalType": "bytes", "name": "params", "type": "bytes" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "caller", "type": "address" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "mint", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "mintToTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "rescueTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "scaledBalanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "scaledTotalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "controller", + "type": "address" + } + ], + "name": "setIncentivesController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "transferOnLiquidation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "target", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferUnderlyingTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/ui_workflows/aave/common.py b/ui_workflows/aave/common.py index e25c0fe4..a6ae1fbd 100644 --- a/ui_workflows/aave/common.py +++ b/ui_workflows/aave/common.py @@ -13,6 +13,7 @@ AAVE_POOL_V3_PROXY_ADDRESS = Web3.to_checksum_address("0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2") AAVE_WRAPPED_TOKEN_GATEWAY = Web3.to_checksum_address("0xd322a49006fc828f9b5b37ab215f99b4e5cab19c") AAVE_VARIABLE_DEBT_TOKEN_ADDRESS = Web3.to_checksum_address("0xea51d7853eefb32b6ee06b1c12e6dcca88be0ffe") +AAVE_ATOKEN_ADDRESS = Web3.to_checksum_address("0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8") AAVE_SUPPORTED_TOKENS = [ "ETH", @@ -40,6 +41,10 @@ def get_aave_variable_debt_token_contract(): web3_provider = context.get_web3_provider() return web3_provider.eth.contract(address=AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, abi=load_contract_abi(__file__, "./abis/aave_variable_debt_token.abi.json")) +def get_aave_atoken_contract(): + web3_provider = context.get_web3_provider() + return web3_provider.eth.contract(address=AAVE_ATOKEN_ADDRESS, abi=load_contract_abi(__file__, "./abis/aave_atoken.abi.json")) + def common_aave_validation(token): if (token not in AAVE_SUPPORTED_TOKENS): raise WorkflowValidationError(f"Token {token} not supported by Aave") diff --git a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py index cb1f96eb..a8244196 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py @@ -47,7 +47,6 @@ def confirm_ETH_repay(self): return ContractStepProcessingResult(status="success", tx=tx) - def initiate_ERC20_approval(self): if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address, AAVE_POOL_V3_PROXY_ADDRESS, self.amount)): return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ERC20_repay") diff --git a/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py new file mode 100644 index 00000000..e98ff374 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py @@ -0,0 +1,72 @@ +from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict + +import web3 + +from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult, tenderly_simulate_tx +from database.models import ( + MultiStepWorkflow, WorkflowStepUserActionType +) +from ..common import AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_wrapped_token_gateway_contract, get_aave_pool_v3_address_contract, common_aave_validation, get_aave_atoken_contract + +class AaveWithdrawContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'aave-withdraw' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + self.token = workflow_params["token"] + self.amount = workflow_params["amount"] + + if self.token == "ETH": + initiate_ETH_approval_step = RunnableStep("initiate_ETH_approval", WorkflowStepUserActionType.tx, f"Approve withdraw of {self.amount} ETH on Aave", self.initiate_ETH_approval) + confirm_ETH_withdraw_step = RunnableStep("confirm_ETH_withdraw", WorkflowStepUserActionType.tx, f"Confirm withdraw of {self.amount} ETH on Aave", self.confirm_ETH_withdraw) + steps = [initiate_ETH_approval_step, confirm_ETH_withdraw_step] + + final_step_type = confirm_ETH_withdraw_step.type + else: + confirm_ERC20_withdraw_step = RunnableStep("confirm_ERC20_withdraw", WorkflowStepUserActionType.tx, f"Confirm withdraw of {self.amount} {self.token} on Aave", self.confirm_ERC20_withdraw) + steps = [confirm_ERC20_withdraw_step] + final_step_type = confirm_ERC20_withdraw_step.type + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + common_aave_validation(self.token) + + def initiate_ETH_approval(self): + spender = AAVE_WRAPPED_TOKEN_GATEWAY + amount = int(web3.constants.MAX_INT, 16) + + encoded_data = get_aave_atoken_contract().encodeABI(fn_name='approve', args=[spender, amount]) + tx = { + 'from': self.wallet_address, + 'to': get_aave_atoken_contract().address, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) + + def confirm_ETH_withdraw(self): + pool_address = AAVE_POOL_V3_PROXY_ADDRESS + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + to_address = self.wallet_address + encoded_data = get_aave_wrapped_token_gateway_contract().encodeABI(fn_name='withdrawETH', args=[pool_address, amount, to_address]) + tx = { + 'from': self.wallet_address, + 'to': get_aave_wrapped_token_gateway_contract().address, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) + + def confirm_ERC20_withdraw(self): + asset_address = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + to_address = self.wallet_address + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='withdrawETH', args=[asset_address, amount, to_address]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_POOL_V3_PROXY_ADDRESS, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py index 24214e45..240a5e1c 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py @@ -1,7 +1,4 @@ -""" -Test for Repaying ETH on Aave -""" from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval from ..aave_repay_contract_workflow import AaveRepayContractWorkflow diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py new file mode 100644 index 00000000..e1e420ce --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py @@ -0,0 +1,48 @@ + +from utils import parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_set_usdc_allowance +from ..aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow +from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_withdraw_erc20" +def test_contract_aave_withdraw_erc20(setup_fork): + token = "USDC" + amount = 100 + workflow_params = {"token": token, "amount": amount} + + # Pre-deposit USDC in order to test withdraw + aave_set_usdc_allowance(parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount)) + multistep_result = AaveSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Confirm withdraw of 100 USDC on Aave" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py new file mode 100644 index 00000000..4dc9d9d5 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py @@ -0,0 +1,56 @@ + +from utils import parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow +from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_withdraw_eth" +def test_contract_aave_withdraw_eth(setup_fork): + token = "ETH" + amount = 0.5 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Approve withdraw of 0.5 ETH on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + assert multistep_result.description == "Confirm withdraw of 0.5 ETH on Aave" + assert multistep_result.is_final_step == True + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" \ No newline at end of file From 71f4270199493540ccb6d192ac530bedf6aafa1e Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Thu, 8 Jun 2023 19:01:06 -0500 Subject: [PATCH 05/13] wip --- README.md | 2 +- tools/index_widget.py | 6 +++--- ui_workflows/aave/__init__.py | 3 +-- .../aave/contract_abi_integration/__init__.py | 5 ++++- ui_workflows/base/base_multi_step_mixin.py | 5 ++++- ui_workflows/multistep_handler.py | 12 ++++++++++-- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 44df32c8..61529ea5 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ docker-compose up ``` -# DRAFT +# DRAFT TODO - the Weaviate vector index may not have been updated with the latest widgets.txt changes, so anytime the file is changed the following needs to be done to allow semantic search to query against the latest state of the widgets:- - Bump up the index version on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py#L9 (so use `WidgetV11` as v10 already taken by pending PR) - Similarly, bump up the index version on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/config.py#L6 diff --git a/tools/index_widget.py b/tools/index_widget.py index e0f99ced..9ba858f9 100644 --- a/tools/index_widget.py +++ b/tools/index_widget.py @@ -220,11 +220,11 @@ def replace_match(m: re.Match) -> Union[str | Generator]: return str(fetch_yields(*params)) elif command == aave.AaveSupplyContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='supply')) - elif command == aave.AaveBorrowUIWorkflow.WORKFLOW_TYPE: + elif command == aave.AaveBorrowContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='borrow')) - elif command == 'aave-repay': + elif command == aave.AaveRepayContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='repay')) - elif command == 'aave-withdraw': + elif command == aave.AaveWithdrawContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='withdraw')) elif command == 'ens-from-address': return str(ens_from_address(*params)) diff --git a/ui_workflows/aave/__init__.py b/ui_workflows/aave/__init__.py index 3e8b17ee..e25ea201 100644 --- a/ui_workflows/aave/__init__.py +++ b/ui_workflows/aave/__init__.py @@ -1,2 +1 @@ -from .ui_integration import AaveSupplyUIWorkflow, AaveBorrowUIWorkflow -from .contract_abi_integration import AaveSupplyContractWorkflow \ No newline at end of file +from .contract_abi_integration import * \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/__init__.py b/ui_workflows/aave/contract_abi_integration/__init__.py index 40202826..4e5a264f 100644 --- a/ui_workflows/aave/contract_abi_integration/__init__.py +++ b/ui_workflows/aave/contract_abi_integration/__init__.py @@ -1 +1,4 @@ -from .aave_supply_contract_workflow import AaveSupplyContractWorkflow \ No newline at end of file +from .aave_supply_contract_workflow import AaveSupplyContractWorkflow +from .aave_borrow_contract_workflow import AaveBorrowContractWorkflow +from .aave_repay_contract_workflow import AaveRepayContractWorkflow +from .aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow \ No newline at end of file diff --git a/ui_workflows/base/base_multi_step_mixin.py b/ui_workflows/base/base_multi_step_mixin.py index 0248fffd..77378aa1 100644 --- a/ui_workflows/base/base_multi_step_mixin.py +++ b/ui_workflows/base/base_multi_step_mixin.py @@ -218,7 +218,10 @@ def _handle_step_replace(self, *args, **kwargs) -> Union[ContractStepProcessingR if self.workflow_approach == WorkflowApproach.UI: return runnable_step.function(page, browser_context, replacement_extra_params) else: - return runnable_step.function(replacement_extra_params) + if replacement_extra_params: + return runnable_step.function(replacement_extra_params) + else: + return runnable_step.function() def _find_runnable_step_index_by_step_type(self, step_type) -> int: return [i for i,s in enumerate(self.runnable_steps) if s.type == step_type][0] diff --git a/ui_workflows/multistep_handler.py b/ui_workflows/multistep_handler.py index d6f9ca27..87e2c416 100644 --- a/ui_workflows/multistep_handler.py +++ b/ui_workflows/multistep_handler.py @@ -52,8 +52,12 @@ def process_multistep_workflow(payload: MessagePayload, send_message: Callable): result = register_ens_domain(workflow_params['domain'], user_chat_message_id, workflow_db_obj, step) elif workflow_type == aave.AaveSupplyContractWorkflow.WORKFLOW_TYPE: result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "supply", user_chat_message_id, workflow_db_obj, step) - elif workflow_type == aave.AaveBorrowUIWorkflow.WORKFLOW_TYPE: + elif workflow_type == aave.AaveBorrowContractWorkflow.WORKFLOW_TYPE: result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "borrow", user_chat_message_id, workflow_db_obj, step) + elif workflow_type == aave.AaveRepayContractWorkflow.WORKFLOW_TYPE: + result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "repay", user_chat_message_id, workflow_db_obj, step) + elif workflow_type == aave.AaveWithdrawContractWorkflow.WORKFLOW_TYPE: + result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "withdraw", user_chat_message_id, workflow_db_obj, step) else: raise Exception(f'Workflow type {workflow_type} not supported.') @@ -93,7 +97,11 @@ def exec_aave_operation(token: str, amount: str, operation: Literal["supply", "b if operation == 'supply': wf = aave.AaveSupplyContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) elif operation == 'borrow': - wf = aave.AaveBorrowUIWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) + wf = aave.AaveBorrowContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) + elif operation == 'repay': + wf = aave.AaveRepayContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) + elif operation == 'withdraw': + wf = aave.AaveWithdrawContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) else: raise Exception(f'Operation {operation} not supported.') From b638e74e34b6f6a37c491a4c64127ba630e11f16 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Thu, 8 Jun 2023 19:48:14 -0500 Subject: [PATCH 06/13] wip --- .../aave_borrow_contract_workflow.py | 2 +- .../aave_repay_contract_workflow.py | 13 +------------ .../aave_supply_contract_workflow.py | 15 +-------------- .../aave_withdraw_contract_workflow.py | 9 ++++++++- .../base/base_multi_step_contract_workflow.py | 17 +++++++++++++++-- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py index da31c8f6..cf4a7b47 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py @@ -67,7 +67,7 @@ def initiate_ETH_approval(self): return ContractStepProcessingResult(status="success", tx=tx) - def confirm_ETH_borrow(self, extra_params=None): + def confirm_ETH_borrow(self): pool_address = AAVE_POOL_V3_PROXY_ADDRESS amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) interest_rate_mode = 2 # Variable diff --git a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py index a8244196..4616ac10 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py @@ -48,18 +48,7 @@ def confirm_ETH_repay(self): return ContractStepProcessingResult(status="success", tx=tx) def initiate_ERC20_approval(self): - if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address, AAVE_POOL_V3_PROXY_ADDRESS, self.amount)): - return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ERC20_repay") - - spender = AAVE_POOL_V3_PROXY_ADDRESS - value = parse_token_amount(self.wallet_chain_id, self.token, self.amount) - encoded_data = generate_erc20_approve_encoded_data(self.web3_provider, self.wallet_chain_id, self.token, spender, value) - tx = { - 'from': self.wallet_address, - 'to': get_token_address(self.wallet_chain_id, self.token), - 'data': encoded_data, - } - return ContractStepProcessingResult(status="success", tx=tx) + return self._initiate_ERC20_approval(AAVE_POOL_V3_PROXY_ADDRESS, self.token, self.amount, 'confirm_ERC20_repay') def confirm_ERC20_repay(self): asset_address = get_token_address(self.wallet_chain_id, self.token) diff --git a/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py index 4671445a..817fc34a 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py @@ -64,20 +64,7 @@ def confirm_ETH_supply_step(self) -> ContractStepProcessingResult: def initiate_ERC20_approval_step(self): """Initiate approval of ERC20 token to be spent by Aave""" - - if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address, AAVE_POOL_V3_PROXY_ADDRESS, self.amount)): - return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ERC20_supply") - - spender = AAVE_POOL_V3_PROXY_ADDRESS - value = parse_token_amount(self.wallet_chain_id, self.token, self.amount) - encoded_data = generate_erc20_approve_encoded_data(self.web3_provider, self.wallet_chain_id, self.token, spender, value) - tx = { - 'from': self.wallet_address, - 'to': get_token_address(self.wallet_chain_id, self.token), - 'data': encoded_data, - } - return ContractStepProcessingResult(status="success", tx=tx) - + return self._initiate_ERC20_approval(AAVE_POOL_V3_PROXY_ADDRESS, self.token, self.amount, 'confirm_ERC20_supply') def confirm_ERC20_supply_step(self, extra_params=None) -> ContractStepProcessingResult: """Confirm supply of ERC20 token""" diff --git a/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py index e98ff374..0a9c5550 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py @@ -33,7 +33,14 @@ def _general_workflow_validation(self): common_aave_validation(self.token) def initiate_ETH_approval(self): + owner = self.wallet_address spender = AAVE_WRAPPED_TOKEN_GATEWAY + + allowance = get_aave_atoken_contract().functions.allowance(owner, spender).call() + + if parse_token_amount(self.wallet_chain_id, self.token, self.amount) <= allowance: + return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ETH_withdraw") + amount = int(web3.constants.MAX_INT, 16) encoded_data = get_aave_atoken_contract().encodeABI(fn_name='approve', args=[spender, amount]) @@ -62,7 +69,7 @@ def confirm_ERC20_withdraw(self): asset_address = get_token_address(self.wallet_chain_id, self.token) amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) to_address = self.wallet_address - encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='withdrawETH', args=[asset_address, amount, to_address]) + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='withdraw', args=[asset_address, amount, to_address]) tx = { 'from': self.wallet_address, 'to': AAVE_POOL_V3_PROXY_ADDRESS, diff --git a/ui_workflows/base/base_multi_step_contract_workflow.py b/ui_workflows/base/base_multi_step_contract_workflow.py index 3ae6e451..e3d84ce8 100644 --- a/ui_workflows/base/base_multi_step_contract_workflow.py +++ b/ui_workflows/base/base_multi_step_contract_workflow.py @@ -2,7 +2,7 @@ import uuid from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict -from utils import estimate_gas +from utils import parse_token_amount, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance, get_token_address from database.models import db_session, WorkflowStep, WorkflowStepStatus, MultiStepWorkflow from .common import WorkflowStepClientPayload, RunnableStep, ContractStepProcessingResult, MultiStepResult, WorkflowValidationError, compute_abi_abspath @@ -20,4 +20,17 @@ def run(self) -> MultiStepResult: return BaseMultiStepMixin.run(self) def _run(self) -> MultiStepResult: - return BaseMultiStepMixin._run(self) \ No newline at end of file + return BaseMultiStepMixin._run(self) + + def _initiate_ERC20_approval(self, spender, token, amount, replace_with_step_type): + if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, token, self.wallet_address, spender, amount)): + return ContractStepProcessingResult(status="replace", replace_with_step_type=replace_with_step_type) + + value = parse_token_amount(self.wallet_chain_id, token, amount) + encoded_data = generate_erc20_approve_encoded_data(self.web3_provider, self.wallet_chain_id, token, spender, value) + tx = { + 'from': self.wallet_address, + 'to': get_token_address(self.wallet_chain_id, token), + 'data': encoded_data, + } + return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file From 3757c4b92ae4b0443e4813742784b99f650da406 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Thu, 8 Jun 2023 23:07:27 -0500 Subject: [PATCH 07/13] wip --- ui_workflows/aave/common.py | 21 ++++- .../aave_borrow_contract_workflow.py | 20 +--- .../aave_contract_error_codes.json | 92 +++++++++++++++++++ .../aave_repay_contract_workflow.py | 8 +- .../aave_supply_contract_workflow.py | 6 +- .../aave_withdraw_contract_workflow.py | 8 +- .../tests/test_aave_borrow_erc20.py | 4 - .../test_aave_borrow_eth_no_collateral.py | 26 ++++++ ui_workflows/base/__init__.py | 2 +- ui_workflows/base/base_contract_workflow.py | 47 ++++++++-- ui_workflows/base/base_multi_step_mixin.py | 18 ++-- ui_workflows/base/common.py | 29 +++++- 12 files changed, 227 insertions(+), 54 deletions(-) create mode 100644 ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json create mode 100644 ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py diff --git a/ui_workflows/aave/common.py b/ui_workflows/aave/common.py index a6ae1fbd..15ff7e7c 100644 --- a/ui_workflows/aave/common.py +++ b/ui_workflows/aave/common.py @@ -1,3 +1,4 @@ +import os import re import json from typing import Optional, Union, Literal @@ -7,8 +8,13 @@ from web3 import Web3 from utils import load_contract_abi -from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError +from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError, ContractStepProcessingResult +def load_aave_contract_error_codes(): + with open(os.path.join(os.path.dirname(__file__), "./contract_abi_integration/aave_contract_error_codes.json")) as f: + return json.load(f) + +AAVE_CONTRACT_ERROR_CODES = load_aave_contract_error_codes() FIVE_SECONDS = 5000 AAVE_POOL_V3_PROXY_ADDRESS = Web3.to_checksum_address("0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2") AAVE_WRAPPED_TOKEN_GATEWAY = Web3.to_checksum_address("0xd322a49006fc828f9b5b37ab215f99b4e5cab19c") @@ -104,3 +110,16 @@ def aave_revoke_eth_approval(): # https://docs.aave.com/developers/tokens/debttoken#approvedelegation aave_set_eth_approval(0) +def aave_parse_contract_error(code: str): + # Ref: https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/helpers/Errors.sol + print(f"Aave error code: {code}") + if code not in AAVE_CONTRACT_ERROR_CODES: + return f"Unexpected Aave error. Check with support" + return AAVE_CONTRACT_ERROR_CODES[code] + +def aave_check_for_error_and_compute_result(self, tx): + error_message = self._simulate_tx_for_error_check(tx) + if error_message: + return ContractStepProcessingResult(status="error", error_msg=aave_parse_contract_error(error_message)) + else: + return ContractStepProcessingResult(status="success", tx=tx) diff --git a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py index cf4a7b47..cadd566e 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py @@ -3,11 +3,11 @@ import web3 from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address -from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult, tenderly_simulate_tx +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult from database.models import ( db_session, MultiStepWorkflow, WorkflowStep, WorkflowStepStatus, WorkflowStepUserActionType, ChatMessage, ChatSession, SystemConfig ) -from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract, common_aave_validation +from ..common import AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract, common_aave_validation, aave_parse_contract_error, aave_check_for_error_and_compute_result class AaveBorrowContractWorkflow(BaseMultiStepContractWorkflow): """ @@ -21,14 +21,12 @@ def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: s self.amount = workflow_params["amount"] if self.token == "ETH": - # check_ETH_liquidation_risk_step = RunnableStep("check_ETH_liquidation_risk", WorkflowStepUserActionType.acknowledge, f"Acknowledge liquidation risk due to high borrow amount of {self.amount} ETH on Aave", self.check_ETH_liquidation_risk) initiate_ETH_approval_step = RunnableStep("initiate_ETH_approval", WorkflowStepUserActionType.tx, f"Approve borrow of {self.amount} ETH on Aave", self.initiate_ETH_approval) confirm_ETH_borrow_step = RunnableStep("confirm_ETH_borrow", WorkflowStepUserActionType.tx, f"Confirm borrow of {self.amount} ETH on Aave", self.confirm_ETH_borrow) steps = [initiate_ETH_approval_step, confirm_ETH_borrow_step] final_step_type = "confirm_ETH_borrow" else: - # check_ERC20_liquidation_risk = RunnableStep("check_ERC20_liquidation_risk", WorkflowStepUserActionType.acknowledge, f"Acknowledge liquidation risk due to high borrow amount of {self.amount} {self.token} on Aave", self.check_ERC20_liquidation_risk) confirm_ERC20_borrow_step = RunnableStep("confirm_ERC20_borrow", WorkflowStepUserActionType.tx, f"Confirm borrow of {self.amount} {self.token} on Aave", self.confirm_ERC20_borrow) steps = [confirm_ERC20_borrow_step] final_step_type = "confirm_ERC20_borrow" @@ -38,13 +36,6 @@ def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: s def _general_workflow_validation(self): common_aave_validation(self.token) - # TODO: add a check to verify that the user has deposited collateral - - - def check_ETH_liquidation_risk(self): - # TODO: add a check to get the health factor and ensure that it is not too high - pass - def initiate_ETH_approval(self): """Initiate approval for ETH token""" @@ -78,11 +69,8 @@ def confirm_ETH_borrow(self): 'to': AAVE_WRAPPED_TOKEN_GATEWAY, 'data': encoded_data, } - - return ContractStepProcessingResult(status="success", tx=tx) - def check_ERC20_liquidation_risk(self, page, browser_context): - pass + return aave_check_for_error_and_compute_result(self, tx) def confirm_ERC20_borrow(self): asset_address = get_token_address(self.wallet_chain_id, self.token) @@ -98,4 +86,4 @@ def confirm_ERC20_borrow(self): 'data': encoded_data, } - return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file + return aave_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json b/ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json new file mode 100644 index 00000000..c0a2fae3 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json @@ -0,0 +1,92 @@ +{ + "1": "The caller of the function is not a pool admin", + "2": "The caller of the function is not an emergency admin", + "3": "The caller of the function is not a pool or emergency admin", + "4": "The caller of the function is not a risk or pool admin", + "5": "The caller of the function is not an asset listing or pool admin", + "6": "The caller of the function is not a bridge", + "7": "Pool addresses provider is not registered", + "8": "Invalid id for the pool addresses provider", + "9": "Address is not a contract", + "10": "The caller of the function is not the pool configurator", + "11": "The caller of the function is not an AToken", + "12": "The address of the pool addresses provider is invalid", + "13": "Invalid return value of the flashloan executor function", + "14": "Reserve has already been added to reserve list", + "15": "Maximum amount of reserves in the pool reached", + "16": "Zero eMode category is reserved for volatile heterogeneous assets", + "17": "Invalid eMode category assignment to asset", + "18": "The liquidity of the reserve needs to be 0", + "19": "Invalid flashloan premium", + "20": "Invalid risk parameters for the reserve", + "21": "Invalid risk parameters for the eMode category", + "22": "Invalid bridge protocol fee", + "23": "The caller of this function must be a pool", + "24": "Invalid amount to mint", + "25": "Invalid amount to burn", + "26": "Amount must be greater than 0", + "27": "Action requires an active reserve", + "28": "Action cannot be performed because the reserve is frozen", + "29": "Action cannot be performed because the reserve is paused", + "30": "Borrowing is not enabled", + "31": "Stable borrowing is not enabled", + "32": "User cannot withdraw more than the available balance", + "33": "Invalid interest rate mode selected", + "34": "The collateral balance is 0", + "35": "Health factor is lesser than the liquidation threshold", + "36": "There is not enough collateral to cover a new borrow", + "37": "Collateral is (mostly) the same currency that is being borrowed", + "38": "The requested amount is greater than the max loan size in stable rate mode", + "39": "For repayment of a specific type of debt, the user needs to have debt that type", + "40": "To repay on behalf of a user an explicit amount to repay is needed", + "41": "User does not have outstanding stable rate debt on this reserve", + "42": "User does not have outstanding variable rate debt on this reserve", + "43": "The underlying balance needs to be greater than 0", + "44": "Interest rate rebalance conditions were not met", + "45": "Health factor is not below the threshold", + "46": "The collateral chosen cannot be liquidated", + "47": "User did not borrow the specified currency", + "49": "Inconsistent flashloan parameters", + "50": "Borrow cap is exceeded", + "51": "Supply cap is exceeded", + "52": "Unbacked mint cap is exceeded", + "53": "Debt ceiling is exceeded", + "54": "Claimable rights over underlying not zero (aToken supply or accruedToTreasury)", + "55": "Stable debt supply is not zero", + "56": "Variable debt supply is not zero", + "57": "Ltv validation failed", + "58": "Inconsistent eMode category", + "59": "Price oracle sentinel validation failed", + "60": "Asset is not borrowable in isolation mode", + "61": "Reserve has already been initialized", + "62": "User is in isolation mode", + "63": "Invalid ltv parameter for the reserve", + "64": "Invalid liquidity threshold parameter for the reserve", + "65": "Invalid liquidity bonus parameter for the reserve", + "66": "Invalid decimals parameter of the underlying asset of the reserve", + "67": "Invalid reserve factor parameter for the reserve", + "68": "Invalid borrow cap for the reserve", + "69": "Invalid supply cap for the reserve", + "70": "Invalid liquidation protocol fee for the reserve", + "71": "Invalid eMode category for the reserve", + "72": "Invalid unbacked mint cap for the reserve", + "73": "Invalid debt ceiling for the reserve", + "74": "Invalid reserve index", + "75": "ACL admin cannot be set to the zero address", + "76": "Array parameters that should be equal length are not", + "77": "Zero address not valid", + "78": "Invalid expiration", + "79": "Invalid signature", + "80": "Operation not supported", + "81": "Debt ceiling is not zero", + "82": "Asset is not listed", + "83": "Invalid optimal usage ratio", + "84": "Invalid optimal stable to total debt ratio", + "85": "The underlying asset cannot be rescued", + "86": "Reserve has already been added to reserve list", + "87": "The token implementation pool address and the pool address provided by the initializing pool do not match", + "88": "Stable borrowing is enabled", + "89": "User is trying to borrow multiple assets including a siloed one", + "90": "The total debt of the reserve needs to be 0", + "91": "FlashLoaning for this asset is disabled" +} diff --git a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py index 4616ac10..f551ec46 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py @@ -3,11 +3,11 @@ import web3 from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance -from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult, tenderly_simulate_tx +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult from database.models import ( MultiStepWorkflow, WorkflowStepUserActionType ) -from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract, common_aave_validation +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_wrapped_token_gateway_contract, get_aave_pool_v3_address_contract, common_aave_validation, aave_check_for_error_and_compute_result class AaveRepayContractWorkflow(BaseMultiStepContractWorkflow): WORKFLOW_TYPE = 'aave-repay' @@ -45,7 +45,7 @@ def confirm_ETH_repay(self): 'value': hexify_token_amount(self.wallet_chain_id, self.token, self.amount) } - return ContractStepProcessingResult(status="success", tx=tx) + return aave_check_for_error_and_compute_result(self, tx) def initiate_ERC20_approval(self): return self._initiate_ERC20_approval(AAVE_POOL_V3_PROXY_ADDRESS, self.token, self.amount, 'confirm_ERC20_repay') @@ -63,4 +63,4 @@ def confirm_ERC20_repay(self): 'data': encoded_data, } - return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file + return aave_check_for_error_and_compute_result(self, tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py index 817fc34a..adcb6e0a 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py @@ -11,7 +11,7 @@ db_session, MultiStepWorkflow, WorkflowStepUserActionType ) from ...base import BaseMultiStepContractWorkflow, WorkflowStepClientPayload, RunnableStep, WorkflowValidationError, ContractStepProcessingResult -from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_pool_v3_address_contract, get_aave_wrapped_token_gateway_contract +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_pool_v3_address_contract, get_aave_wrapped_token_gateway_contract, aave_check_for_error_and_compute_result class AaveSupplyContractWorkflow(BaseMultiStepContractWorkflow): """ @@ -60,7 +60,7 @@ def confirm_ETH_supply_step(self) -> ContractStepProcessingResult: 'value': hexify_token_amount(self.wallet_chain_id, self.token, self.amount), } - return ContractStepProcessingResult(status="success", tx=tx) + return aave_check_for_error_and_compute_result(self, tx) def initiate_ERC20_approval_step(self): """Initiate approval of ERC20 token to be spent by Aave""" @@ -80,4 +80,4 @@ def confirm_ERC20_supply_step(self, extra_params=None) -> ContractStepProcessing 'data': encoded_data, } - return ContractStepProcessingResult(status="success", tx=tx) + return aave_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py index 0a9c5550..e30ee5fb 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py @@ -3,11 +3,11 @@ import web3 from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance -from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult, tenderly_simulate_tx +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult from database.models import ( MultiStepWorkflow, WorkflowStepUserActionType ) -from ..common import AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_wrapped_token_gateway_contract, get_aave_pool_v3_address_contract, common_aave_validation, get_aave_atoken_contract +from ..common import AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_wrapped_token_gateway_contract, get_aave_pool_v3_address_contract, common_aave_validation, get_aave_atoken_contract, aave_check_for_error_and_compute_result class AaveWithdrawContractWorkflow(BaseMultiStepContractWorkflow): WORKFLOW_TYPE = 'aave-withdraw' @@ -63,7 +63,7 @@ def confirm_ETH_withdraw(self): 'data': encoded_data, } - return ContractStepProcessingResult(status="success", tx=tx) + return aave_check_for_error_and_compute_result(self, tx) def confirm_ERC20_withdraw(self): asset_address = get_token_address(self.wallet_chain_id, self.token) @@ -76,4 +76,4 @@ def confirm_ERC20_withdraw(self): 'data': encoded_data, } - return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file + return aave_check_for_error_and_compute_result(self, tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py index 8c08fc3f..e1a6d4aa 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py @@ -1,7 +1,3 @@ - -""" -Test for borrowing ETH on Aave -""" from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py new file mode 100644 index 00000000..42a848a7 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py @@ -0,0 +1,26 @@ + +""" +Test for borrowing ETH on Aave + +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_eth_no_collateral" +def test_contract_aave_borrow_eth_no_collateral(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-approve ETH borrow to set the test environment for borrow + aave_set_eth_approval(1*10**18) + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # TODO: This will fail as Tenderly is not able to simulate the tx on the latest block for a fork, investigate why + assert multistep_result.status == 'error' + assert multistep_result.error_msg == 'The collateral balance is 0' + + + \ No newline at end of file diff --git a/ui_workflows/base/__init__.py b/ui_workflows/base/__init__.py index 955df871..6f32d5b2 100644 --- a/ui_workflows/base/__init__.py +++ b/ui_workflows/base/__init__.py @@ -2,7 +2,7 @@ from .common import ( WorkflowStepClientPayload, RunnableStep, StepProcessingResult, MultiStepResult, Result, WorkflowValidationError, ContractStepProcessingResult, - tenderly_simulate_tx, compute_abi_abspath, setup_mock_db_objects, process_result_and_simulate_tx, + tenderly_simulate_tx_on_fork, compute_abi_abspath, setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, revoke_erc20_approval, set_erc20_allowance, advance_fork_time_secs, advance_fork_blocks, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID, USDC_ADDRESS diff --git a/ui_workflows/base/base_contract_workflow.py b/ui_workflows/base/base_contract_workflow.py index 158cede3..0df73f19 100644 --- a/ui_workflows/base/base_contract_workflow.py +++ b/ui_workflows/base/base_contract_workflow.py @@ -1,11 +1,13 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict from dataclasses import dataclass -import json +import requests -import context +from web3 import Web3, exceptions -from web3 import Web3 +import context +import env +from utils import TENDERLY_API_KEY class BaseContractWorkflow(ABC): """Grandparent base class for contract workflows. Do not directly use this class, use either BaseSingleStepContractWorkflow or BaseMultiStepContractWorkflow class""" @@ -18,11 +20,6 @@ def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: s self.workflow_params = workflow_params self.web3_provider = context.get_web3_provider() - def run(self) -> Any: - """Main function to call to run the workflow.""" - ret = self._run() - return ret - @abstractmethod def _run(self) -> Any: """Implement the contract interaction logic here.""" @@ -32,3 +29,37 @@ def _run(self) -> Any: def _general_workflow_validation(self): """Override this method to perform any common validation checks for all steps in the workflow before running them""" + def run(self) -> Any: + """Main function to call to run the workflow.""" + ret = self._run() + return ret + + def _simulate_tx_for_error_check(self, tx: Dict) -> Optional[str]: + if env.is_prod(): + tenderly_simulate_api_url = f"https://api.tenderly.co/api/v1/account/Yield/project/chatweb3/simulate" + else: + tenderly_simulate_api_url = f"https://api.tenderly.co/api/v1/account/Yield/project/chatweb3/fork/{context.get_web3_fork_id()}/simulate" + + payload = { + "save": False, + "save_if_fails": False, + "simulation_type": "full", + "block_number": "latest", + "network_id": self.wallet_chain_id, + "from": tx['from'], + "to": tx['to'], + "input": tx['data'], + "value": tx.get('value', 0), + } + + res = requests.post(tenderly_simulate_api_url, json=payload, headers={'X-Access-Key': TENDERLY_API_KEY}) + + if res.status_code == 200: + simulation_data = res.json() + error_message = simulation_data['transaction']['error_message'] + return error_message + else: + return None + + + diff --git a/ui_workflows/base/base_multi_step_mixin.py b/ui_workflows/base/base_multi_step_mixin.py index 77378aa1..dd40c7cc 100644 --- a/ui_workflows/base/base_multi_step_mixin.py +++ b/ui_workflows/base/base_multi_step_mixin.py @@ -122,14 +122,16 @@ def _run(self, *args, **kwargs) -> MultiStepResult: else: # For contract ABI approach tx = processing_result.tx - try: - tx['gas'] = estimate_gas(tx) - except Exception: - # If gas usage estimation fails, use fallback arbitary gas limit to attempt tx - tx['gas'] = FALLBACK_GAS_LIMIT - - if tx and "value" not in tx: - tx['value'] = "0x0" + + if tx: + try: + tx['gas'] = estimate_gas(tx) + except Exception: + # If gas usage estimation fails, use fallback arbitary gas limit to attempt tx + tx['gas'] = FALLBACK_GAS_LIMIT + + if "value" not in tx: + tx['value'] = "0x0" computed_user_description = processing_result.override_user_description or self.curr_step_description diff --git a/ui_workflows/base/common.py b/ui_workflows/base/common.py index 64c2bb84..f3f011ff 100644 --- a/ui_workflows/base/common.py +++ b/ui_workflows/base/common.py @@ -8,6 +8,7 @@ import requests import context +from utils import TENDERLY_API_KEY from database.models import db_session, ChatMessage, ChatSession, SystemConfig from database.models import (MultiStepWorkflow) @@ -72,7 +73,7 @@ class WorkflowValidationError(Exception): class WorkflowFailed(Exception): pass -def tenderly_simulate_tx(wallet_address: str, tx: Dict) -> str: +def tenderly_simulate_tx_on_fork(wallet_address: str, tx: Dict) -> str: payload = { "jsonrpc": "2.0", "method": "eth_sendTransaction", @@ -98,12 +99,30 @@ def tenderly_simulate_tx(wallet_address: str, tx: Dict) -> str: fork_web3 = Web3(Web3.HTTPProvider(fork_rpc_url)) receipt = fork_web3.eth.wait_for_transaction_receipt(tx_hash) - print("receipt:", receipt) + get_latest_tx_payload = { + "jsonrpc": "2.0", + "method": "evm_getLatest", + "params": [] + } + + res = requests.post(fork_rpc_url, json=get_latest_tx_payload) + res.raise_for_status() + + tenderly_simulation_id = res.json()['result'] + + tenderly_dashboard_link = f"https://dashboard.tenderly.co/Yield/chatweb3/fork/{fork_id}/simulation/{tenderly_simulation_id}" - print("Tenderly TxHash:", tx_hash) + print("Tenderly simulation dashboard link:", tenderly_dashboard_link) + # Tx Error handling if receipt['status'] == 0: - raise Exception(f"Transaction failed, tx_hash: {tx_hash}, check fork for more details - https://dashboard.tenderly.co/Yield/chatweb3/fork/{fork_id}") + tenderly_simulation_api = f"https://api.tenderly.co/api/v1/account/Yield/project/chatweb3/fork/{fork_id}/simulation/{tenderly_simulation_id}" + res = requests.get(tenderly_simulation_api, headers={'X-Access-Key': TENDERLY_API_KEY}) + error_message = 'n/a' + if res.status_code == 200: + simulation_data = res.json() + error_message = simulation_data['transaction']['error_message'] + raise Exception(f"Transaction failed, error_message: {error_message}, check fork for more details - {tenderly_dashboard_link}") return tx_hash @@ -169,7 +188,7 @@ def compute_abi_abspath(wf_file_path, abi_relative_path): def process_result_and_simulate_tx(wallet_address, result: Union[Result, MultiStepResult]) -> Optional[str]: if result.status == "success": - tx_hash = tenderly_simulate_tx(wallet_address, result.tx) + tx_hash = tenderly_simulate_tx_on_fork(wallet_address, result.tx) print("Workflow successful") return tx_hash elif result.status == "terminated": From eae3d97990737274a75f919e6b0eb5819568d9d1 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Fri, 9 Jun 2023 00:04:33 -0500 Subject: [PATCH 08/13] wip --- .../tests/test_aave_borrow_erc20.py | 8 +++++++- .../tests/test_aave_borrow_eth_no_approval.py | 8 +++++++- .../tests/test_aave_borrow_eth_with_approval.py | 8 +++++++- .../tests/test_aave_repay_erc20.py | 8 ++++++-- .../tests/test_aave_repay_eth.py | 8 ++++++-- .../tests/test_aave_supply_erc20_no_approval.py | 11 +++++++---- .../tests/test_aave_supply_erc20_with_approval.py | 13 +++++++------ .../tests/test_aave_supply_eth.py | 11 +++++++---- .../tests/test_aave_supply_extreme_amount.py | 2 -- .../tests/test_aave_withdraw_erc20.py | 8 ++++++-- .../tests/test_aave_withdraw_eth.py | 10 ++++++++-- 11 files changed, 68 insertions(+), 27 deletions(-) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py index e1a6d4aa..b1e7b2f8 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py @@ -1,3 +1,5 @@ +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow @@ -8,6 +10,8 @@ def test_contract_aave_borrow_erc20(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Pre-supply ETH to Aave to setup the test environment for borrow aave_supply_eth_for_borrow_test() @@ -40,4 +44,6 @@ def test_contract_aave_borrow_erc20(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert dai_balance_end == dai_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py index d517ca43..ea496cdd 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py @@ -2,6 +2,8 @@ """ Test for borrowing ETH on Aave """ +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow @@ -12,6 +14,8 @@ def test_contract_aave_borrow_eth_no_approval(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Pre-supply ETH to Aave to setup the test environment for borrow aave_supply_eth_for_borrow_test() @@ -51,4 +55,6 @@ def test_contract_aave_borrow_eth_no_approval(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, figure out how to fetch decoded tx data from Tenderly and assert the amount processed + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert eth_balance_end == eth_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py index 3359f290..6f1aab17 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py @@ -2,6 +2,8 @@ """ Test for borrowing ETH on Aave """ +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow @@ -12,6 +14,8 @@ def test_contract_aave_borrow_eth_with_approval(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Pre-supply ETH to Aave to setup the test environment for borrow aave_supply_eth_for_borrow_test() @@ -54,4 +58,6 @@ def test_contract_aave_borrow_eth_with_approval(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, figure out how to fetch decoded tx data from Tenderly and assert the amount processed + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert eth_balance_end == eth_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py index 240a5e1c..00aa2066 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py @@ -1,4 +1,6 @@ +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval from ..aave_repay_contract_workflow import AaveRepayContractWorkflow @@ -15,9 +17,10 @@ def test_contract_aave_repay_erc20(setup_fork): # First borrow in order to test repay multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() - process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + usdt_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() # Assert what the user will see on the UI @@ -60,4 +63,5 @@ def test_contract_aave_repay_erc20(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + usdt_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert usdt_balance_end == usdt_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py index b93dd52d..e3a86cfd 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py @@ -2,6 +2,8 @@ """ Test for Repaying ETH on Aave """ +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval from ..aave_repay_contract_workflow import AaveRepayContractWorkflow @@ -21,9 +23,10 @@ def test_contract_aave_repay_eth(setup_fork): # First borrow in order to test repay multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() - process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() # Assert what the user will see on the UI @@ -53,4 +56,5 @@ def test_contract_aave_repay_eth(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert eth_balance_end == eth_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py index 961be490..acac7e90 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py @@ -1,9 +1,8 @@ """ Test for supplying an ERC20 token on Aave without approval step as it is already pre-approved """ -from logging import basicConfig, INFO -from dataclasses import dataclass, asdict - +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID from ...common import aave_set_usdc_allowance from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow @@ -14,6 +13,8 @@ def test_contract_aave_supply_erc20_no_approval(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + usdc_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Set allowance for pre-approval aave_set_usdc_allowance(int(amount * 10 ** 6)) @@ -43,4 +44,6 @@ def test_contract_aave_supply_erc20_no_approval(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + usdc_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert usdc_balance_end == usdc_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py index c4e9e1fe..75989e59 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py @@ -1,10 +1,8 @@ - -import re -import time -import json """ Test for supplying an ERC20 token on Aave with approval step """ +import context +from utils import get_token_balance, parse_token_amount import os import requests from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable @@ -16,12 +14,14 @@ from ...common import aave_revoke_usdc_approval -# Invoke this with python3 -m pytest -s -k "test_aave_supply_erc20_with_approval" +# Invoke this with python3 -m pytest -s -k "test_contract_aave_supply_erc20_with_approval" def test_contract_aave_supply_erc20_with_approval(setup_fork): token = "USDC" amount = 0.1 workflow_params = {"token": token, "amount": amount} + usdc_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Make sure to revoke any USDC pre-approval to ensure Aave UI is in the correct state to show approval flow aave_revoke_usdc_approval() @@ -69,4 +69,5 @@ def test_contract_aave_supply_erc20_with_approval(setup_fork): # Final state of workflow should be terminated assert multi_step_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + usdc_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert usdc_balance_end == usdc_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py index b512bae0..1d63a920 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py @@ -2,9 +2,9 @@ """ Test for supplying a ETH on Aave """ -import re - -from ....base import setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID from ...contract_abi_integration import AaveSupplyContractWorkflow # Invoke this with python3 -m pytest -s -k "test_contract_aave_supply_eth" @@ -13,6 +13,8 @@ def test_contract_aave_supply_eth(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + multi_step_result = AaveSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() assert multi_step_result.description == "Confirm supply of 0.1 ETH on Aave" @@ -41,4 +43,5 @@ def test_contract_aave_supply_eth(setup_fork): # Final state of workflow should be terminated assert multi_step_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert eth_balance_end == eth_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py index f1377ec1..a3ef593b 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py @@ -2,8 +2,6 @@ """ Test for supplying an ETH amount greater than account balance on Aave """ -import re - import context from ....base import setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py index e1e420ce..460066b0 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py @@ -1,5 +1,6 @@ -from utils import parse_token_amount +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_set_usdc_allowance from ..aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow @@ -16,6 +17,8 @@ def test_contract_aave_withdraw_erc20(setup_fork): multistep_result = AaveSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + usdc_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() # Assert what the user will see on the UI @@ -45,4 +48,5 @@ def test_contract_aave_withdraw_erc20(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + usdc_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert usdc_balance_end == usdc_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py index 4dc9d9d5..7eac5fc9 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py @@ -1,5 +1,6 @@ -from utils import parse_token_amount +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval from ..aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow @@ -14,6 +15,8 @@ def test_contract_aave_withdraw_eth(setup_fork): # Pre-supply ETH to Aave to setup the test environment for borrow aave_supply_eth_for_borrow_test() + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() # Assert what the user will see on the UI @@ -53,4 +56,7 @@ def test_contract_aave_withdraw_eth(setup_fork): multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() # Final state of workflow should be terminated - assert multistep_result.status == "terminated" \ No newline at end of file + assert multistep_result.status == "terminated" + + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert eth_balance_end == eth_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) From 985ace58b8631313514bfc8705c2a5bff2397754 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Fri, 9 Jun 2023 00:17:06 -0500 Subject: [PATCH 09/13] wip --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 61529ea5..72ea3133 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ cd docker docker-compose up ``` - -# DRAFT TODO -- the Weaviate vector index may not have been updated with the latest widgets.txt changes, so anytime the file is changed the following needs to be done to allow semantic search to query against the latest state of the widgets:- - - Bump up the index version on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py#L9 (so use `WidgetV11` as v10 already taken by pending PR) - - Similarly, bump up the index version on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/config.py#L6 - - Finally, run this command on https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py (`python3 -c "from index import widgets; widgets.backfill()"`) +## Steps to add new widget command +- Update `widgets.txt` with the widget command details +- Bump up the widget index version in `INDEX_NAME` https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py#L9  +- Similarly, bump up the index version in `index_name`  https://github.com/yieldprotocol/chatweb3-backend/blob/dev/config.py#L6 +- Run this Python command to update our Weaviate Vector DB with the new widget `python3 -c "from index import widgets; widgets.backfill()"` +- Add the widget's handler function in `replace_match()` https://github.com/yieldprotocol/chatweb3-backend/blob/dev/tools/index_widget.py#L189 From 58f6dea06f898f8528cedce30b0e1851ab990770 Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Fri, 9 Jun 2023 09:36:40 -0500 Subject: [PATCH 10/13] more changes --- ui_workflows/aave/common.py | 6 +++--- ui_workflows/base/base_contract_workflow.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui_workflows/aave/common.py b/ui_workflows/aave/common.py index 15ff7e7c..c3ece696 100644 --- a/ui_workflows/aave/common.py +++ b/ui_workflows/aave/common.py @@ -8,7 +8,7 @@ from web3 import Web3 from utils import load_contract_abi -from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError, ContractStepProcessingResult +from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError, ContractStepProcessingResult, BaseContractWorkflow def load_aave_contract_error_codes(): with open(os.path.join(os.path.dirname(__file__), "./contract_abi_integration/aave_contract_error_codes.json")) as f: @@ -117,8 +117,8 @@ def aave_parse_contract_error(code: str): return f"Unexpected Aave error. Check with support" return AAVE_CONTRACT_ERROR_CODES[code] -def aave_check_for_error_and_compute_result(self, tx): - error_message = self._simulate_tx_for_error_check(tx) +def aave_check_for_error_and_compute_result(contract_workflow: BaseContractWorkflow, tx): + error_message = contract_workflow._simulate_tx_for_error_check(tx) if error_message: return ContractStepProcessingResult(status="error", error_msg=aave_parse_contract_error(error_message)) else: diff --git a/ui_workflows/base/base_contract_workflow.py b/ui_workflows/base/base_contract_workflow.py index 0df73f19..aa3e6c24 100644 --- a/ui_workflows/base/base_contract_workflow.py +++ b/ui_workflows/base/base_contract_workflow.py @@ -44,7 +44,6 @@ def _simulate_tx_for_error_check(self, tx: Dict) -> Optional[str]: "save": False, "save_if_fails": False, "simulation_type": "full", - "block_number": "latest", "network_id": self.wallet_chain_id, "from": tx['from'], "to": tx['to'], @@ -53,10 +52,11 @@ def _simulate_tx_for_error_check(self, tx: Dict) -> Optional[str]: } res = requests.post(tenderly_simulate_api_url, json=payload, headers={'X-Access-Key': TENDERLY_API_KEY}) - + if res.status_code == 200: simulation_data = res.json() - error_message = simulation_data['transaction']['error_message'] + transaction = simulation_data['transaction'] + error_message = transaction.get('error_message') return error_message else: return None From 76f0650eed32b0d8ccbc054338e5cec0547ecfdb Mon Sep 17 00:00:00 2001 From: Sagar Shah Date: Fri, 9 Jun 2023 10:40:48 -0500 Subject: [PATCH 11/13] changes to tenderly simulation logic --- ui_workflows/base/base_contract_workflow.py | 6 ++++- ui_workflows/base/common.py | 26 +++++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ui_workflows/base/base_contract_workflow.py b/ui_workflows/base/base_contract_workflow.py index aa3e6c24..8a1a15ca 100644 --- a/ui_workflows/base/base_contract_workflow.py +++ b/ui_workflows/base/base_contract_workflow.py @@ -8,6 +8,7 @@ import context import env from utils import TENDERLY_API_KEY +from .common import get_latest_simulation_id_on_fork class BaseContractWorkflow(ABC): """Grandparent base class for contract workflows. Do not directly use this class, use either BaseSingleStepContractWorkflow or BaseMultiStepContractWorkflow class""" @@ -43,7 +44,7 @@ def _simulate_tx_for_error_check(self, tx: Dict) -> Optional[str]: payload = { "save": False, "save_if_fails": False, - "simulation_type": "full", + "simulation_type": "quick", "network_id": self.wallet_chain_id, "from": tx['from'], "to": tx['to'], @@ -51,6 +52,9 @@ def _simulate_tx_for_error_check(self, tx: Dict) -> Optional[str]: "value": tx.get('value', 0), } + if not env.is_prod(): + payload["root"] = get_latest_simulation_id_on_fork(context.get_web3_tenderly_fork_url()) + res = requests.post(tenderly_simulate_api_url, json=payload, headers={'X-Access-Key': TENDERLY_API_KEY}) if res.status_code == 200: diff --git a/ui_workflows/base/common.py b/ui_workflows/base/common.py index f3f011ff..f28a7885 100644 --- a/ui_workflows/base/common.py +++ b/ui_workflows/base/common.py @@ -99,16 +99,7 @@ def tenderly_simulate_tx_on_fork(wallet_address: str, tx: Dict) -> str: fork_web3 = Web3(Web3.HTTPProvider(fork_rpc_url)) receipt = fork_web3.eth.wait_for_transaction_receipt(tx_hash) - get_latest_tx_payload = { - "jsonrpc": "2.0", - "method": "evm_getLatest", - "params": [] - } - - res = requests.post(fork_rpc_url, json=get_latest_tx_payload) - res.raise_for_status() - - tenderly_simulation_id = res.json()['result'] + tenderly_simulation_id = get_latest_simulation_id_on_fork(fork_rpc_url) tenderly_dashboard_link = f"https://dashboard.tenderly.co/Yield/chatweb3/fork/{fork_id}/simulation/{tenderly_simulation_id}" @@ -126,6 +117,21 @@ def tenderly_simulate_tx_on_fork(wallet_address: str, tx: Dict) -> str: return tx_hash +def get_latest_simulation_on_fork(fork_rpc_url): + get_latest_tx_payload = { + "jsonrpc": "2.0", + "method": "evm_getLatest", + "params": [] + } + + res = requests.post(fork_rpc_url, json=get_latest_tx_payload) + res.raise_for_status() + + return res.json() + +def get_latest_simulation_id_on_fork(fork_rpc_url): + return get_latest_simulation_on_fork(fork_rpc_url)['result'] + def advance_fork_blocks(num_blocks) -> None: fork_rpc_url = context.get_web3_tenderly_fork_url() From f2eb0b417596227efd6b6aa958edca7f0402c791 Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 14 Jun 2023 11:07:27 +0100 Subject: [PATCH 12/13] draft: savingsdai as a derivation from aave integration --- knowledge_base/widgets.txt | 10 ++++ ui_workflows/savings_dai/__init__.py | 1 + .../savings_dai/abis/savings_dai.abi.json | 1 + ui_workflows/savings_dai/common.py | 24 ++++++++ .../contract_abi_integration/__init__.py | 2 + .../savings_dai_deposit_contract_workflow.py | 55 +++++++++++++++++++ .../savings_dai_redeem_contract_workflow.py | 55 +++++++++++++++++++ .../tests/__init__.py | 0 utils/crypto_token.py | 4 ++ 9 files changed, 152 insertions(+) create mode 100644 ui_workflows/savings_dai/__init__.py create mode 100644 ui_workflows/savings_dai/abis/savings_dai.abi.json create mode 100644 ui_workflows/savings_dai/common.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/__init__.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/tests/__init__.py diff --git a/knowledge_base/widgets.txt b/knowledge_base/widgets.txt index bd84e714..8bd4743b 100644 --- a/knowledge_base/widgets.txt +++ b/knowledge_base/widgets.txt @@ -216,6 +216,16 @@ Required Parameters: -{token}: token to withdraw -{amount}: quantity to withdraw --- +Widget magic command: <|savings-dai-deposit({amount})|> +Description of widget: This widget is used when the user wants to deposit DAI into SavingsDAI to get SavingsDAI, that can be later redeemed for a profit. +Required Parameters: +-{amount}: amount of DAI to deposit +--- +Widget magic command: <|savings-dai-redeem({amount})|> +Description of widget: This widget is used when the user wants to redeem their SavingsDAI to get DAI, realizing a profit. +Required Parameters: +-{amount}: amount of SavingsDAI to redeem +--- Widget magic command: <|display-yield-farm({project},{network}, {token}, {amount})|> Description of widget: This widget is only to be used for the Compound project to allow the user to yield farm by putting tokens or depositing tokens of a certain amount into the Compound project Required Parameters: diff --git a/ui_workflows/savings_dai/__init__.py b/ui_workflows/savings_dai/__init__.py new file mode 100644 index 00000000..e25ea201 --- /dev/null +++ b/ui_workflows/savings_dai/__init__.py @@ -0,0 +1 @@ +from .contract_abi_integration import * \ No newline at end of file diff --git a/ui_workflows/savings_dai/abis/savings_dai.abi.json b/ui_workflows/savings_dai/abis/savings_dai.abi.json new file mode 100644 index 00000000..e4da8a03 --- /dev/null +++ b/ui_workflows/savings_dai/abis/savings_dai.abi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_daiJoin","type":"address"},{"internalType":"address","name":"_pot","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"asset","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"convertToAssets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"convertToShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dai","outputs":[{"internalType":"contract DaiLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"daiJoin","outputs":[{"internalType":"contract DaiJoinLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deploymentChainId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"}],"name":"deposit","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"maxDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"maxMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"maxRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"maxWithdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"}],"name":"mint","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pot","outputs":[{"internalType":"contract PotLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"previewDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"previewMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"previewRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"previewWithdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"redeem","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalAssets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/ui_workflows/savings_dai/common.py b/ui_workflows/savings_dai/common.py new file mode 100644 index 00000000..7a683617 --- /dev/null +++ b/ui_workflows/savings_dai/common.py @@ -0,0 +1,24 @@ +import os +import re +import json +from typing import Optional, Union, Literal + +import context +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError +from web3 import Web3 + +from utils import load_contract_abi +from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError, ContractStepProcessingResult, BaseContractWorkflow + +SAVINGS_DAI_ADDRESS = Web3.to_checksum_address("0x83F20F44975D03b1b09e64809B757c47f942BEeA") + +def get_savings_dai_address_contract(): + web3_provider = context.get_web3_provider() + return web3_provider.eth.contract(address=SAVINGS_DAI_ADDRESS, abi=load_contract_abi(__file__, "./abis/savings_dai.abi.json")) + +def savings_dai_check_for_error_and_compute_result(contract_workflow: BaseContractWorkflow, tx): + error_message = contract_workflow._simulate_tx_for_error_check(tx) + if error_message: + return ContractStepProcessingResult(status="error", error_msg=error_message) + else: + return ContractStepProcessingResult(status="success", tx=tx) diff --git a/ui_workflows/savings_dai/contract_abi_integration/__init__.py b/ui_workflows/savings_dai/contract_abi_integration/__init__.py new file mode 100644 index 00000000..78eb9365 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/__init__.py @@ -0,0 +1,2 @@ +from .savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow +from .savings_dai_redeem_contract_workflow import SavingsDaiRedeemContractWorkflow \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py new file mode 100644 index 00000000..afff6a72 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py @@ -0,0 +1,55 @@ +import re +from logging import basicConfig, INFO +from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable +from dataclasses import dataclass, asdict + +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError + +import env +from utils import get_token_balance, estimate_gas, parse_token_amount, hexify_token_amount, has_sufficient_erc20_allowance, generate_erc20_approve_encoded_data, get_token_address +from database.models import ( + db_session, MultiStepWorkflow, WorkflowStepUserActionType +) +from ...base import BaseMultiStepContractWorkflow, WorkflowStepClientPayload, RunnableStep, WorkflowValidationError, ContractStepProcessingResult +from ..common import SAVINGS_DAI_ADDRESS, get_savings_dai_address_contract, savings_dai_check_for_error_and_compute_result + +class SavingsDaiDepositContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'savings-dai-deposit' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + serlf.token = "DAI" + self.amount = workflow_params["amount"] + + # The only token that can be deposited is DAI, you have to handle approval before final confirmation + initiate_erc20_approval_step = RunnableStep("initiate_ERC20_approval", WorkflowStepUserActionType.tx, f"Approve deposit of {self.amount} {self.token} on SavingsDAI", self.initiate_erc20_approval_step) + confirm_erc4626_deposit_step = RunnableStep("confirm_ERC4626_deposit", WorkflowStepUserActionType.tx, f"Confirm deposit of {self.amount} {self.token} on SavingsDAI", self.confirm_erc4626_deposit_step) + steps = [initiate_erc20_approval_step, confirm_erc4626_deposit_step] + + final_step_type = "confirm_erc4626_deposit" + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + if (get_token_balance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address) < parse_token_amount(self.wallet_chain_id, self.token, self.amount)): + raise WorkflowValidationError(f"Insufficient {self.token} balance in wallet") + + + def initiate_ERC20_approval_step(self): + """Initiate approval of ERC20 token to be taken by SavingsDAI""" + return self._initiate_ERC20_approval(SAVINGS_DAI_ADDRESS, self.token, self.amount, 'confirm_ERC20_approval') + + def confirm_ERC20_supply_step(self, extra_params=None) -> ContractStepProcessingResult: + """Confirm deposit of DAI""" + + asset = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + on_behalf_of = self.wallet_address + referral_code = 0 + encoded_data = get_savings_dai_address_contract().encodeABI(fn_name='deposit', args=[amount, self.wallet_address]) + tx = { + 'from': self.wallet_address, + 'to': SAVINGS_DAI_ADDRESS, + 'data': encoded_data, + } + + return savings_dai_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py new file mode 100644 index 00000000..a681ac77 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py @@ -0,0 +1,55 @@ +import re +from logging import basicConfig, INFO +from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable +from dataclasses import dataclass, asdict + +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError + +import env +from utils import get_token_balance, estimate_gas, parse_token_amount, hexify_token_amount, has_sufficient_erc20_allowance, generate_erc20_approve_encoded_data, get_token_address +from database.models import ( + db_session, MultiStepWorkflow, WorkflowStepUserActionType +) +from ...base import BaseMultiStepContractWorkflow, WorkflowStepClientPayload, RunnableStep, WorkflowValidationError, ContractStepProcessingResult +from ..common import SAVINGS_DAI_ADDRESS, get_savings_dai_address_contract, savings_dai_check_for_error_and_compute_result + +class SavingsDaiRedeemContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'savings-dai-redeem' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + serlf.token = "SavingsDAI" + self.amount = workflow_params["amount"] + + # The only token that can be redeemed is SavingsDAI, you have to handle approval before final confirmation + initiate_erc20_approval_step = RunnableStep("initiate_ERC20_approval", WorkflowStepUserActionType.tx, f"Approve redemption of {self.amount} {self.token} on SavingsDAI", self.initiate_erc20_approval_step) + confirm_erc4626_redeem_step = RunnableStep("confirm_ERC4626_redeem", WorkflowStepUserActionType.tx, f"Confirm redemption of {self.amount} {self.token} on SavingsDAI", self.confirm_erc20_deposit_step) + steps = [initiate_erc20_approval_step, confirm_erc4626_redeem_step] + + final_step_type = "confirm_erc4626_redeem" + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + if (get_token_balance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address) < parse_token_amount(self.wallet_chain_id, self.token, self.amount)): + raise WorkflowValidationError(f"Insufficient {self.token} balance in wallet") + + + def initiate_ERC20_approval_step(self): + """Initiate approval of ERC20 token to be taken by SavingsDAI""" + return self._initiate_ERC20_approval(SAVINGS_DAI_ADDRESS, self.token, self.amount, 'confirm_ERC20_approval') + + def confirm_ERC4626_redeem_step(self, extra_params=None) -> ContractStepProcessingResult: + """Confirm redemption of SavingsDAI""" + + asset = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + on_behalf_of = self.wallet_address + referral_code = 0 + encoded_data = get_savings_dai_address_contract().encodeABI(fn_name='redeem', args=[amount, self.wallet_address]) + tx = { + 'from': self.wallet_address, + 'to': SAVINGS_DAI_ADDRESS, + 'data': encoded_data, + } + + return savings_dai_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/__init__.py b/ui_workflows/savings_dai/contract_abi_integration/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/crypto_token.py b/utils/crypto_token.py index e058309e..495f93aa 100644 --- a/utils/crypto_token.py +++ b/utils/crypto_token.py @@ -46,6 +46,10 @@ "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "decimals": 8 }, + "SavingsDAI": { + "address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "decimals": 18 + }, } def parse_token_amount(chain_id: int, token: str, amount: str) -> int: From b6da3e3033d1b32aad7182b28d3342bdbdce9f13 Mon Sep 17 00:00:00 2001 From: alcueca Date: Wed, 14 Jun 2023 11:21:44 +0100 Subject: [PATCH 13/13] draft: tests --- ...test_savings_dai_deposit_extreme_amount.py | 22 ++++++ .../test_savings_dai_deposit_no_approval.py | 49 +++++++++++++ .../test_savings_dai_deposit_with_approval.py | 73 +++++++++++++++++++ .../tests/test_savings_dai_redeem.py | 57 +++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py create mode 100644 ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py new file mode 100644 index 00000000..3ed1fcf9 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py @@ -0,0 +1,22 @@ + +""" +Test for depositing a DAI amount greater than account balance on SavingsDai +""" +import context +from ....base import setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID +from ..savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_deposit_extreme_amount" +def test_contract_savings_dai_deposit_extreme_amount(setup_fork): + web3_provider = context.get_web3_provider() + current_dai_balance = web3_provider.dai.get_balance(TEST_WALLET_ADDRESS) + test_extreme_dai_amount = current_dai_balance + 10*10**18 # Add 10 dai to the current available balance + + token = "DAI" + amount = test_extreme_dai_amount + workflow_params = {"token": token, "amount": amount} + + multistep_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.error_msg == "Insufficient dai balance in wallet" \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py new file mode 100644 index 00000000..6e110a50 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py @@ -0,0 +1,49 @@ +""" +Test for depositing DAI on SavingsDAI without approval step as it is already pre-approved +""" +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID +from ...common import savings_dai_set_dai_allowance +from ..savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_deposit_no_approval" +def test_contract_savings_dai_deposit_no_approval(setup_fork): + token = "DAI" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Set allowance for pre-approval + savings_dai_set_dai_allowance(int(amount * 10 ** 18)) + + # Confirm deposit of DAI with no approval step + multistep_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + assert multistep_result.description == "Confirm deposit of 0.1 DAI on SavingsDAI" + + assert multistep_result.is_final_step + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert dai_balance_end == dai_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py new file mode 100644 index 00000000..74867123 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py @@ -0,0 +1,73 @@ +""" +Test for depositing Dai on SavingsDai with approval step +""" +import context +from utils import get_token_balance, parse_token_amount +import os +import requests +from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable +from dataclasses import dataclass, asdict + +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID + +from ..savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow + +from ...common import savings_dai_revoke_dai_approval + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_deposit_with_approval" +def test_contract_savings_dai_deposit_with_approval(setup_fork): + token = "DAI" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Make sure to revoke any DAI pre-approval to ensure SavingsDai UI is in the correct state to show approval flow + savings_dai_revoke_dai_approval() + + # Step 1 - Approve deposit of DAI + multi_step_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multi_step_result.description == "Approve deposit of 0.1 DAI on SavingsDai" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multi_step_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multi_step_result.step_id, + "type": multi_step_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multi_step_result.workflow_id + + multi_step_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Step 2 - Process Step 1 response from FE and continue to Step 2 which is to confirm deposit of DAI + multi_step_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multi_step_workflow, curr_step_client_payload).run() + + assert multi_step_result.description == "Confirm deposit of 0.1 DAI on SavingsDai" + + assert multi_step_result.is_final_step + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multi_step_result) + + curr_step_client_payload = { + "id": multi_step_result.step_id, + "type": multi_step_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multi_step_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multi_step_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multi_step_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert dai_balance_end == dai_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py new file mode 100644 index 00000000..bb6a0b45 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py @@ -0,0 +1,57 @@ + +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import savings_dai_set_dai_allowance +from ..savings_dai_withdraw_contract_workflow import SavingsDaiRedeemContractWorkflow +from ..savings_dai_supply_contract_workflow import SavingsDaiSupplyContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_withdraw_erc20" +def test_contract_savings_dai_withdraw_erc20(setup_fork): + token = "DAI" + amount = 100 + workflow_params = {"token": token, "amount": amount} + + # Pre-deposit DAI in order to test withdraw + savings_dai_set_dai_allowance(parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount)) + multistep_result = SavingsDaiSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Redeem all SavingsDAI obtained + token = "SavingsDAI" + amount = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + workflow_params = {"token": token, "amount": amount} + + multistep_result = SavingsDaiRedeemContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == f"Confirm redemption of {amount} SavingsDAI on SavingsDai" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = SavingsDaiRedeemContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, "DAI", TEST_WALLET_ADDRESS) + assert dai_balance_end > dai_balance_start