Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lagoon deposit flow #264

Merged
merged 8 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api/lagoon/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Lagoon protocol vaults integration.

eth_defi.lagoon.vault
eth_defi.lagoon.deployment
eth_defi.lagoon.analysis
114 changes: 114 additions & 0 deletions eth_defi/lagoon/analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Analyse Lagoon protocol deposits and redemptions.

- To track our treasury balance

- Find Lagoon events here https://github.com/hopperlabsxyz/lagoon-v0/blob/b790b1c1fbb51a101b0c78a4bb20e8700abed054/src/vault/primitives/Events.sol
"""
import datetime
from dataclasses import dataclass
from decimal import Decimal

from hexbytes import HexBytes
from web3._utils.events import EventLogErrorFlags

from eth_defi.lagoon.vault import LagoonVault
from eth_defi.timestamp import get_block_timestamp
from eth_defi.token import TokenDetails


@dataclass(slots=True, frozen=True)
class LagoonSettlementEvent:
"""Capture Lagoon vault flow when it is settled.

- Use to adjust vault treasury balances for internal accounting
- We do not capture individual users
"""

#: Chain we checked
chain_id: int

#: settleDeposit() transaction by the asset managre
tx_hash: HexBytes

#: When the settlement was done
block_number: int

#: When the settlement was done
timestamp: datetime.datetime

#: Vault address
vault: LagoonVault

#: How much new underlying was added to the vault
deposited: Decimal

#: How much was redeemed successfully
redeemed: Decimal

#: Shares added for new investor
shares_minted: Decimal

#: Shares burned for redemptions
shares_burned: Decimal

@property
def underlying(self) -> TokenDetails:
"""Get USDC."""
return self.vault.underlying_token

@property
def share_token(self) -> TokenDetails:
"""Get USDC."""
return self.vault.share_token

def get_serialiable_diagnostics_data(self) -> dict:
"""JSON serialisable diagnostics data for logging"""
return {
"chain_id": self.chain_id,
"block_number": self.block_number,
"timestamp": self.timestamp,
"tx_hash": self.tx_hash.hex(),
"vault": self.vault.vault_address,
"underlying": self.underlying.address,
"share_token": self.share_token.address,
"deposited": self.deposited,
"redeemed": self.redeemed,
"shares_minted": self.shares_minted,
"shares_burned": self.shares_minted,
}

def analyse_vault_flow_in_settlement(
vault: LagoonVault,
tx_hash: HexBytes,
) -> LagoonSettlementEvent:
"""Extract deposit and redeem events from a settlement transaction"""
web3 = vault.web3
receipt = web3.eth.get_transaction_receipt(tx_hash)
assert receipt is not None, f"Cannot find tx: {tx_hash}"
assert isinstance(tx_hash, HexBytes), f"Got {tx_hash}"

deposits = vault.vault_contract.events.SettleDeposit().process_receipt(receipt, errors=EventLogErrorFlags.Discard)
redeems = vault.vault_contract.events.SettleRedeem().process_receipt(receipt, errors=EventLogErrorFlags.Discard)

assert len(deposits) == 1, f"Does not look like settleDeposit() tx: {tx_hash.hex()}"

new_deposited_raw = sum(log["args"]["assetsDeposited"] for log in deposits)
new_minted_raw = sum(log["args"]["sharesMinted"] for log in deposits)

new_redeem_raw = sum(log["args"]["assetsWithdrawed"] for log in redeems)
new_burned_raw = sum(log["args"]["sharesBurned"] for log in redeems)

block_number = receipt["blockNumber"]
timestamp = get_block_timestamp(web3, block_number)

return LagoonSettlementEvent(
chain_id=vault.chain_id,
tx_hash=tx_hash,
block_number=block_number,
timestamp=timestamp,
vault=vault,
deposited=vault.underlying_token.convert_to_decimals(new_deposited_raw),
redeemed=vault.underlying_token.convert_to_decimals(new_redeem_raw),
shares_minted=vault.share_token.convert_to_decimals(new_minted_raw),
shares_burned=vault.share_token.convert_to_decimals(new_burned_raw),
)
16 changes: 12 additions & 4 deletions eth_defi/lagoon/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from eth.typing import BlockRange
from eth_typing import HexAddress, BlockIdentifier, ChecksumAddress
from fontTools.unicodedata import block
from hexbytes import HexBytes
from web3 import Web3
from web3.contract import Contract
from web3.contract.contract import ContractFunction
Expand Down Expand Up @@ -125,9 +126,9 @@ def fetch_safe(self, address) -> Safe:
def chain_id(self) -> int:
return self.spec.chain_id

@property
@cached_property
def vault_address(self) -> HexAddress:
return self.spec.vault_address
return Web3.to_checksum_address(self.spec.vault_address)

@property
def name(self) -> str:
Expand Down Expand Up @@ -462,14 +463,19 @@ def post_valuation_and_settle(
valuation: Decimal,
asset_manager: HexAddress,
gas=1_000_000,
):
) -> HexBytes:
"""Do both new valuation and settle.

- Quickhand method for asset_manager code

- Only after this we can read back

- Broadcasts two transactions
- Broadcasts two transactions and waits for the confirmation

- If there is not enough USDC to redeem, the second transaction will fail with revert

:return:
The transaction hash of the settlement transaction
"""

assert isinstance(valuation, Decimal)
Expand All @@ -482,6 +488,8 @@ def post_valuation_and_settle(
tx_hash = bound_func.transact({"from": asset_manager, "gas": gas})
assert_transaction_success_with_explanation(self.web3, tx_hash)

return tx_hash

def request_deposit(self, depositor: HexAddress, raw_amount: int) -> ContractFunction:
"""Build a deposit transction.

Expand Down
51 changes: 51 additions & 0 deletions eth_defi/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import warnings

import cachetools
from web3.contract.contract import ContractFunction

with warnings.catch_warnings():
# DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
Expand Down Expand Up @@ -151,6 +152,52 @@ def fetch_balance_of(self, address: HexAddress | str, block_identifier="latest")
raw_amount = self.contract.functions.balanceOf(address).call(block_identifier=block_identifier)
return self.convert_to_decimals(raw_amount)

def transfer(
self,
to: HexAddress | str,
amount: Decimal,
) -> ContractFunction:
"""Prepare a ERC20.transfer() transaction with human-readable amount.

Example:

.. code-block:: python

another_new_depositor = web3.eth.accounts[6]
tx_hash = base_usdc.transfer(another_new_depositor, Decimal(500)).transact({"from": usdc_holder, "gas": 100_000})
assert_transaction_success_with_explanation(web3, tx_hash)

:return:
Bound contract function you need to turn to a tx
"""
assert isinstance(amount, Decimal), f"Give amounts in decimal, got {type(amount)}"
to = Web3.to_checksum_address(to)
raw_amount = self.convert_to_raw(amount)
return self.contract.functions.transfer(to, raw_amount)

def approve(
self,
to: HexAddress | str,
amount: Decimal,
) -> ContractFunction:
"""Prepare a ERC20.approve() transaction with human-readable amount.

Example:

.. code-block:: python

usdc_amount = Decimal(9.00)
tx_hash = usdc.approve(vault.address, usdc_amount).transact({"from": depositor})
assert_transaction_success_with_explanation(web3, tx_hash)

:return:
Bound contract function you need to turn to a tx
"""
assert isinstance(amount, Decimal), f"Give amounts in decimal, got {type(amount)}"
to = Web3.to_checksum_address(to)
raw_amount = self.convert_to_raw(amount)
return self.contract.functions.approve(to, raw_amount)

def fetch_raw_balance_of(self, address: HexAddress | str, block_identifier="latest") -> Decimal:
"""Get an address token balance.

Expand Down Expand Up @@ -257,6 +304,10 @@ def fetch_erc20_details(

The function should not raise an exception as long as the underlying node connection does not fail.

.. note ::

Always give ``chain_id`` when possible. Otherwise the caching of data is inefficient.

Example:

.. code-block:: python
Expand Down
35 changes: 35 additions & 0 deletions eth_defi/velvet/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Vault metadata https://api.velvet.capital/api/v3/portfolio/0xbdd3897d59843220927f0915aa943ddfa1214703r

"""
import logging
from functools import cached_property

import requests
Expand All @@ -30,6 +31,9 @@
DEFAULT_VELVET_API_URL = "https://eventsapi.velvetdao.xyz/api/v3"


logger = logging.getLogger(__name__)


class VelvetBadConfig(Exception):
"""Likely wrong vault address given"""

Expand Down Expand Up @@ -131,6 +135,10 @@ def info(self) -> VelvetVaultInfo:
def vault_address(self) -> HexAddress:
return self.info["vaultAddress"]

@property
def chain_id(self) -> int:
return self.spec.chain_id

@property
def deposit_manager_address(self) -> HexAddress:
return self.info["depositManager"]
Expand Down Expand Up @@ -201,6 +209,7 @@ def prepare_swap_with_enso(
from_: HexAddress | str | None = None,
retries=5,
manage_token_list=True,
swap_all_tripwire_pct=0.01,
) -> dict:
"""Prepare a swap transaction using Enso intent engine and Vevlet API.

Expand All @@ -210,11 +219,37 @@ def prepare_swap_with_enso(
Used with Anvil and unlocked accounts.
"""

logger.info(
"Enso swap. Token %s -> %s, amount %d, swap all is %s",
token_in,
token_out,
swap_amount,
swap_all,
)

assert swap_amount > 0

if manage_token_list:
if swap_all:
assert token_in in remaining_tokens, f"Enso swap full amount: Tried to remove {token_in}, not in the list {remaining_tokens}"
remaining_tokens.remove(token_in)

# Sell all - we need to deal with Velvet specific dust filter,
# or the smart contract will revert
if swap_all:
erc20 = fetch_erc20_details(self.web3, token_in, chain_id=self.chain_id)
onchain_amount = erc20.fetch_raw_balance_of(self.vault_address)
assert onchain_amount > 0, f"{self.vault_address} did not have any onchain token {token_in} to swap "
diff_pct = abs(swap_amount - onchain_amount) / onchain_amount
logger.info(
"Sell all: Applying onchain exact amount dust filter. Onchain balance: %s, swap balance: %s, dust diff %f %%",
onchain_amount,
swap_amount,
diff_pct * 100,
)
assert diff_pct < swap_all_tripwire_pct, f"Onchain balance: {onchain_amount}, asked sell all balance: {swap_all}, diff {diff_pct:%}"
swap_amount = onchain_amount

tx_data = swap_with_velvet_and_enso(
rebalance_address=self.info["rebalancing"],
owner_address=self.owner_address,
Expand Down
Loading