diff --git a/docs/source/api/one_delta/index.rst b/docs/source/api/one_delta/index.rst index eb144d5d..ab7109f8 100644 --- a/docs/source/api/one_delta/index.rst +++ b/docs/source/api/one_delta/index.rst @@ -6,6 +6,7 @@ This is Python documentation for high-level `1delta protocol ContractFunction: + """Supply collateral to Aave + + :param one_delta_deployment: 1delta deployment + :param token: collateral token contract proxy + :param collateral_amount: amount of collateral to be supplied + :param wallet_address: wallet address of the user + :return: multicall contract function to supply collateral + """ + + calls = _build_supply_multicall( + one_delta_deployment=one_delta_deployment, + token=token, + amount=amount, + wallet_address=wallet_address, + ) + return one_delta_deployment.broker_proxy.functions.multicall(calls) + + +def _build_supply_multicall( + one_delta_deployment, + *, + token: Contract, + amount: int, + wallet_address: str, +) -> list[str]: + """Build multicall to supply collateral to Aave + + :param one_delta_deployment: 1delta deployment + :param token: collateral token contract proxy + :param collateral_amount: amount of collateral to be supplied + :param wallet_address: wallet address of the user + :return: list of encoded ABI calls + """ + call_transfer = one_delta_deployment.flash_aggregator.encodeABI( + fn_name="transferERC20In", + args=[ + token.address, + amount, + ], + ) + + call_deposit = one_delta_deployment.flash_aggregator.encodeABI( + fn_name="deposit", + args=[ + token.address, + wallet_address, + ], + ) + + return [call_transfer, call_deposit] + + +def withdraw( + one_delta_deployment: OneDeltaDeployment, + *, + token: Contract, + atoken: Contract, + amount: int, + wallet_address: str, +) -> ContractFunction: + """Withdraw collateral from Aave + + :param one_delta_deployment: 1delta deployment + :param token: collateral token contract proxy + :param atoken: aToken contract proxy + :param collateral_amount: amount of collateral to be withdrawn + :param wallet_address: wallet address of the user + :return: multicall contract function to withdraw collateral + """ + + calls = _build_withdraw_multicall( + one_delta_deployment=one_delta_deployment, + token=token, + atoken=atoken, + amount=amount, + wallet_address=wallet_address, + ) + return one_delta_deployment.broker_proxy.functions.multicall(calls) + + +def _build_withdraw_multicall( + one_delta_deployment, + *, + token: Contract, + atoken: Contract, + amount: int, + wallet_address: str, +) -> list[str]: + """Build multicall to withdraw collateral from Aave + + :param one_delta_deployment: 1delta deployment + :param token: collateral token contract proxy + :param atoken: aToken contract proxy + :param collateral_amount: amount of collateral to be withdrawn, use MAX_AMOUNT to withdraw all collateral + :param wallet_address: wallet address of the user + :return: list of encoded ABI calls + """ + if amount == MAX_AMOUNT: + # use MAX_AMOUNT to make sure the whole balane is swept + call_transfer = one_delta_deployment.flash_aggregator.encodeABI( + fn_name="transferERC20AllIn", + args=[atoken.address], + ) + else: + call_transfer = one_delta_deployment.flash_aggregator.encodeABI( + fn_name="transferERC20In", + args=[atoken.address, amount], + ) + + call_withdraw = one_delta_deployment.flash_aggregator.encodeABI( + fn_name="withdraw", + args=[ + token.address, + wallet_address, + ], + ) + return [call_transfer, call_withdraw] diff --git a/eth_defi/one_delta/position.py b/eth_defi/one_delta/position.py index 118674ad..244f1750 100644 --- a/eth_defi/one_delta/position.py +++ b/eth_defi/one_delta/position.py @@ -9,6 +9,10 @@ from eth_defi.aave_v3.deployment import AaveV3Deployment from eth_defi.one_delta.constants import Exchange, TradeOperation, TradeType from eth_defi.one_delta.deployment import OneDeltaDeployment +from eth_defi.one_delta.lending import ( + _build_supply_multicall, + _build_withdraw_multicall, +) from eth_defi.one_delta.utils import encode_path @@ -92,22 +96,6 @@ def open_short_position( :return: multicall contract function to supply collateral and open the short position """ - call_transfer = one_delta_deployment.flash_aggregator.encodeABI( - fn_name="transferERC20In", - args=[ - collateral_token.address, - collateral_amount, - ], - ) - - call_deposit = one_delta_deployment.flash_aggregator.encodeABI( - fn_name="deposit", - args=[ - collateral_token.address, - wallet_address, - ], - ) - path = encode_path( path=[ borrow_token.address, @@ -128,8 +116,14 @@ def open_short_position( ], ) - calls = [call_transfer, call_deposit, call_swap] - if do_supply is False: + if do_supply is True: + calls = _build_supply_multicall( + one_delta_deployment=one_delta_deployment, + token=collateral_token, + amount=collateral_amount, + wallet_address=wallet_address, + ) + [call_swap] + else: calls = [call_swap] return one_delta_deployment.broker_proxy.functions.multicall(calls) @@ -190,25 +184,13 @@ def close_short_position( if withdraw_collateral_amount == 0: calls = [call_swap] else: - if withdraw_collateral_amount == MAX_AMOUNT: - call_transfer = one_delta_deployment.flash_aggregator.encodeABI( - fn_name="transferERC20AllIn", - args=[atoken.address], - ) - else: - call_transfer = one_delta_deployment.flash_aggregator.encodeABI( - fn_name="transferERC20In", - args=[atoken.address, withdraw_collateral_amount], - ) - - call_withdraw = one_delta_deployment.flash_aggregator.encodeABI( - fn_name="withdraw", - args=[ - collateral_token.address, - wallet_address, - ], + calls = [call_swap] + _build_withdraw_multicall( + one_delta_deployment=one_delta_deployment, + token=collateral_token, + atoken=atoken, + amount=withdraw_collateral_amount, + wallet_address=wallet_address, ) - calls = [call_swap, call_transfer, call_withdraw] return one_delta_deployment.broker_proxy.functions.multicall(calls) diff --git a/tests/one_delta/test_one_delta_lending.py b/tests/one_delta/test_one_delta_lending.py new file mode 100644 index 00000000..ec3e97fa --- /dev/null +++ b/tests/one_delta/test_one_delta_lending.py @@ -0,0 +1,131 @@ +"""Test 1delta lending functions using forked Polygon.""" +import logging +import os +import shutil + +import flaky +import pytest +from eth_account import Account +from eth_account.signers.local import LocalAccount +from eth_typing import HexAddress, HexStr + +from eth_defi.aave_v3.constants import MAX_AMOUNT +from eth_defi.hotwallet import HotWallet +from eth_defi.one_delta.deployment import OneDeltaDeployment +from eth_defi.one_delta.deployment import fetch_deployment as fetch_1delta_deployment +from eth_defi.one_delta.lending import supply, withdraw +from eth_defi.one_delta.position import approve +from eth_defi.provider.anvil import fork_network_anvil, mine +from eth_defi.provider.multi_provider import create_multi_provider_web3 +from eth_defi.token import fetch_erc20_details +from eth_defi.trace import assert_transaction_success_with_explanation + +from .utils import _execute_tx, _print_current_balances + +# https://docs.pytest.org/en/latest/how-to/skipping.html#skip-all-test-functions-of-a-class-or-module +pytestmark = pytest.mark.skipif( + (os.environ.get("JSON_RPC_POLYGON") is None) or (shutil.which("anvil") is None), + reason="Set JSON_RPC_POLYGON env install anvil command to run these tests", +) + +logger = logging.getLogger(__name__) + + +def test_one_delta_supply( + web3, + hot_wallet, + one_delta_deployment, + aave_v3_deployment, + usdc, + ausdc, + weth, + vweth, +): + """Test supply to Aave via 1delta proxy""" + for fn in approve( + one_delta_deployment=one_delta_deployment, + collateral_token=usdc.contract, + borrow_token=weth.contract, + atoken=ausdc.contract, + vtoken=vweth.contract, + aave_v3_deployment=aave_v3_deployment, + ): + _execute_tx(web3, hot_wallet, fn) + + wallet_original_balance = 100_000 * 10**6 + usdc_supply_amount = 10_000 * 10**6 + + supply_fn = supply( + one_delta_deployment=one_delta_deployment, + token=usdc.contract, + amount=usdc_supply_amount, + wallet_address=hot_wallet.address, + ) + _execute_tx(web3, hot_wallet, supply_fn, 500_000) + + assert usdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(wallet_original_balance - usdc_supply_amount) + assert ausdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(usdc_supply_amount) + + +def test_one_delta_withdraw( + web3, + hot_wallet, + one_delta_deployment, + aave_v3_deployment, + usdc, + ausdc, + weth, + vweth, +): + """Test withdraw from Aave via 1delta proxy""" + for fn in approve( + one_delta_deployment=one_delta_deployment, + collateral_token=usdc.contract, + borrow_token=weth.contract, + atoken=ausdc.contract, + vtoken=vweth.contract, + aave_v3_deployment=aave_v3_deployment, + ): + _execute_tx(web3, hot_wallet, fn) + + wallet_original_balance = 100_000 * 10**6 + usdc_supply_amount = 10_000 * 10**6 + + supply_fn = supply( + one_delta_deployment=one_delta_deployment, + token=usdc.contract, + amount=usdc_supply_amount, + wallet_address=hot_wallet.address, + ) + _execute_tx(web3, hot_wallet, supply_fn, 500_000) + + assert usdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(wallet_original_balance - usdc_supply_amount) + assert ausdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(usdc_supply_amount) + + # test partial withdrawal + usdc_partial_withdraw_amount = 4_000 * 10**6 + + withdraw_fn = withdraw( + one_delta_deployment=one_delta_deployment, + token=usdc.contract, + atoken=ausdc.contract, + amount=usdc_partial_withdraw_amount, + wallet_address=hot_wallet.address, + ) + _execute_tx(web3, hot_wallet, withdraw_fn, 500_000) + + assert usdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(wallet_original_balance - usdc_supply_amount + usdc_partial_withdraw_amount) + assert ausdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(usdc_supply_amount - usdc_partial_withdraw_amount) + + # test full withdrawal + withdraw_fn = withdraw( + one_delta_deployment=one_delta_deployment, + token=usdc.contract, + atoken=ausdc.contract, + amount=MAX_AMOUNT, + wallet_address=hot_wallet.address, + ) + _execute_tx(web3, hot_wallet, withdraw_fn, 500_000) + + assert usdc.contract.functions.balanceOf(hot_wallet.address).call() == pytest.approx(wallet_original_balance) + assert ausdc.contract.functions.balanceOf(hot_wallet.address).call() == 0