Skip to content

Commit

Permalink
1delta proxy functions for Aave (#185)
Browse files Browse the repository at this point in the history
Proxy functions to interact with Aave:

- Supply collateral
- Withdraw collateral

Part of tradingstrategy-ai/trade-executor#678
  • Loading branch information
hieuh25 authored Jan 16, 2024
1 parent b01e30d commit 4a7fafb
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 36 deletions.
2 changes: 2 additions & 0 deletions docs/source/api/one_delta/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This is Python documentation for high-level `1delta protocol <https://1delta.io/
Functionality includes

- Opening and closing short positions, utilizing Aave v3 lending pool.
- Supply and withdraw collateral to/from Aave v3 lending pool.

Getting started

Expand All @@ -23,5 +24,6 @@ Getting started
eth_defi.one_delta.deployment
eth_defi.one_delta.position
eth_defi.one_delta.price
eth_defi.one_delta.lending
eth_defi.one_delta.utils

137 changes: 137 additions & 0 deletions eth_defi/one_delta/lending.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""1delta proxy functions to interact with lending pool.
- Supply collateral to lending pool
- Withdraw collateral from lending pool
"""

from web3.contract.contract import Contract, ContractFunction

from eth_defi.aave_v3.constants import MAX_AMOUNT, AaveV3InterestRateMode
from eth_defi.aave_v3.deployment import AaveV3Deployment
from eth_defi.one_delta.deployment import OneDeltaDeployment


def supply(
one_delta_deployment: OneDeltaDeployment,
*,
token: Contract,
amount: int,
wallet_address: str,
) -> 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]
54 changes: 18 additions & 36 deletions eth_defi/one_delta/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
131 changes: 131 additions & 0 deletions tests/one_delta/test_one_delta_lending.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4a7fafb

Please sign in to comment.