Skip to content

Commit

Permalink
Lagoon vault integration (#242)
Browse files Browse the repository at this point in the history
- Integrate safe-eth-py library, so we do not need to copy-paste all Safe integration Python code
- Fetch Safe core metadata, for Lagoon vault info
- Perform a swap using wildcard access module in the Lagoon test setup
  • Loading branch information
miohtama authored Nov 28, 2024
1 parent d779810 commit 5961def
Show file tree
Hide file tree
Showing 16 changed files with 2,588 additions and 1,985 deletions.
19 changes: 10 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: true

- name: Setup Node.js
uses: actions/setup-node@v3
with:
Expand All @@ -44,22 +45,27 @@ jobs:
- name: Install poetry
run: pipx install poetry

- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: '3.12'
cache: 'poetry'

- name: Install dependencies
run: |
poetry env use '3.12'
poetry install --all-extras
- name: Install Ganache
run: yarn global add ganache

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
# pick a nightly release from: https://github.com/foundry-rs/foundry/releases
version: 'nightly-de33b6af53005037b463318d2628b5cfcaf39916'
# version: 'nightly-de33b6af53005037b463318d2628b5cfcaf39916'
version: "nightly-fdd321bac95f0935529164a88faf99d4d5cfa321"

# We also work around race condition for setting up Aave NPM packages.
- name: Setup Aave v3 for tests
Expand All @@ -71,19 +77,14 @@ jobs:
run: |
pnpm --version
make guard in-house
# Run tests parallel.
- name: Run test scripts
- name: Run tests (parallel)
run: |
poetry run pytest --tb=native -n auto
env:
BNB_CHAIN_JSON_RPC: ${{ secrets.BNB_CHAIN_JSON_RPC }}
JSON_RPC_POLYGON_ARCHIVE: ${{ secrets.JSON_RPC_POLYGON_ARCHIVE }}
JSON_RPC_POLYGON: ${{ secrets.JSON_RPC_POLYGON }}
JSON_RPC_ETHEREUM: ${{ secrets.JSON_RPC_ETHEREUM }}
JSON_RPC_BASE: ${{ secrets.JSON_RPC_BASE }}
ETHEREUM_JSON_RPC: ${{ secrets.JSON_RPC_ETHEREUM }}
# Disabled for now
# https://github.com/reviewdog/action-flake8/issues/40
# - name: Run flake8
# uses: reviewdog/action-flake8@v3
# with:
# github_token: ${{ secrets.GITHUB_TOKEN }}
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Current

- Add: Vault abstraction framework to easily work with different onchain vaults. Abstract away vault interactions to its own encapsulating interface.
- Add: Support for [Velvet Capital vaults](https://www.velvet.capital/)
- Add: Support for [Lagoon vaults](https://lagoon.finance/)
- Add: Support for Gnosis Safe [Lagoon vaults](https://safe.global/) via `safe-eth-py` library integration
- Add: Vault abstraction framework to easily work with different onchain vaults. Abstract away vault interactions to its own encapsulating interface.
- Add: `wait_and_broadcast_multiple_nodes_mev_blocker()` for [MEV Blocker](https://mevblocker.io) - because the tx
broadcast must be sequential
- Add: `fetch_erc20_balances_multicall` and `fetch_erc20_balances_fallback` read multiple ERC-20 balances using Multicall library
Expand Down
3 changes: 3 additions & 0 deletions docs/source/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ API documentation
aave_v2/index
aave_v3/index
one_delta/index
safe/index
enzyme/index
lagoon/index
velvet/index
chainlink/index
foundry/index
etherscan/index
Expand Down
10 changes: 10 additions & 0 deletions docs/source/api/lagoon/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Lagoon protocol API
-------------------

Lagoon protocol vaults integration.

.. autosummary::
:toctree: _autosummary_velvet
:recursive:

eth_defi.lagoon.vault
11 changes: 11 additions & 0 deletions docs/source/api/safe/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Gnosis Safe API
---------------

Gnosis Safe multisignature wallet integration.

.. autosummary::
:toctree: _autosummary_safe
:recursive:

eth_defi.safe.trace
eth_defi.safe.safe_compat
12 changes: 12 additions & 0 deletions docs/source/api/velvet/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Velvet Capital API
------------------

Velvet Capital vaults integration.

.. autosummary::
:toctree: _autosummary_velvet
:recursive:

eth_defi.velvet
eth_defi.velvet.config
eth_defi.velvet.deposit
114 changes: 98 additions & 16 deletions eth_defi/lagoon/vault.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
from dataclasses import asdict
from functools import cached_property

from eth_typing import HexAddress, BlockIdentifier
from eth_typing import HexAddress, BlockIdentifier, ChecksumAddress
from web3 import Web3
from web3.contract import Contract
from web3.contract.contract import ContractFunction

from eth_defi.balances import fetch_erc20_balances_fallback
from eth_defi.vault.base import VaultBase, VaultSpec, VaultInfo, TradingUniverse, VaultPortfolio

from safe_eth.safe import Safe

from ..abi import get_deployed_contract, encode_function_call
from ..safe.safe_compat import create_safe_ethereum_client


class LagoonVaultInfo(VaultInfo):
"""TODO: Add Lagoon vault info query"""

#: Address of the Safe multisig the vault is build around
safe_address: HexAddress
#
# Safe multisig core info
#
address: ChecksumAddress
fallback_handler: ChecksumAddress
guard: ChecksumAddress
master_copy: ChecksumAddress
modules: list[ChecksumAddress]
nonce: int
owners: list[ChecksumAddress]
threshold: int
version: str

#
# Lagoon vault info
# TODO
#


class LagoonVault(VaultBase):
Expand All @@ -37,23 +60,42 @@ def has_block_range_event_support(self):
def get_flow_manager(self):
raise NotImplementedError("Velvet does not support individual deposit/redemption events yet")

def fetch_safe(self) -> Safe:
"""Use :py:meth:`safe` property for cached access"""
client = create_safe_ethereum_client(self.web3)
return Safe(
self.safe_address,
client,
)

def fetch_info(self) -> LagoonVaultInfo:
"""Read vault parameters from the chain."""
return {
"safe_address": self.spec.vault_address,
}
"""Use :py:meth:`info` property for cached access"""
info = self.safe.retrieve_all_info()
return asdict(info)

@cached_property
def info(self) -> LagoonVaultInfo:
"""Get info dictionary related to this deployment."""
return self.fetch_info()

@cached_property
def safe(self) -> Safe:
"""Get the underlying Safe object used as an API from safe-eth-py library"""
return self.fetch_safe()

@property
def safe_address(self) -> HexAddress:
return self.info["safe_address"]
def address(self) -> HexAddress:
"""Alias of :py:meth:`safe_address`"""
return self.safe_address

@property
def owner_address(self) -> HexAddress:
return self.info["owner"]
def safe_address(self) -> HexAddress:
"""Get Safe multisig contract address"""
return self.spec.vault_address

@cached_property
def safe_contract(self) -> Contract:
return self.safe.contract

@property
def name(self) -> str:
Expand All @@ -70,18 +112,58 @@ def fetch_portfolio(
) -> VaultPortfolio:
"""Read the current token balances of a vault.
- SHould be supported by all implementations
TODO: This is MVP implementation. For better deposit/redemption tracking switch
to use Lagoon events later.
"""

vault_address = self.info["vaultAddress"]

erc20_balances = fetch_erc20_balances_fallback(
self.web3,
vault_address,
self.safe_address,
universe.spot_token_addresses,
block_identifier=block_identifier,
decimalise=True,
)
return VaultPortfolio(
spot_erc20=erc20_balances,
)

def transact_through_module(
self,
func_call: ContractFunction,
value: int = 0,
operation=0,
) -> ContractFunction:
"""Create a multisig transaction using a module.
- Calls `execTransactionFromModule` on Gnosis Safe contract
- Executes a transaction as a multisig
- Mostly used for testing w/whitelist ignore
:param func_call:
Bound smart contract function call
:param value:
ETH attached to the transaction
:param operation:
Gnosis enum.
.. code-block:: text
library Enum {
enum Operation {
Call,
DelegateCall
}
}
"""
contract_address = func_call.address
data_payload = encode_function_call(func_call, func_call.arguments)
contract = self.safe_contract
bound_func = contract.functions.execTransactionFromModule(
contract_address,
value,
data_payload,
operation,
)
return bound_func
1 change: 1 addition & 0 deletions eth_defi/safe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Gnosis safe integration."""
15 changes: 15 additions & 0 deletions eth_defi/safe/safe_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""safe-eth-py RPC compatibility layer."""
from web3 import Web3

from safe_eth.eth import EthereumClient

def create_safe_ethereum_client(web3: Web3) -> EthereumClient:
"""Safe library wants to use its own funny client.
- Translate Web3 endpoints to EthereumClient
"""
# TODO: Handle MEVProvider
provider = web3.provider
url = provider.endpoint_uri
return EthereumClient(url)
40 changes: 40 additions & 0 deletions eth_defi/safe/trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Safe multisig transaction tranasction error handling."""

from hexbytes import HexBytes
from web3 import Web3

from safe_eth.eth.account_abstraction.constants import EXECUTION_FROM_MODULE_FAILURE_TOPIC, EXECUTION_FROM_MODULE_SUCCESS_TOPIC


def assert_safe_success(web3: Web3, tx_hash: HexBytes):
"""Assert that a Gnosis safe transaction succeeded.
- Gnosis safe swallows any reverts
- We need to extract Gnosis Safe logs from the tx receipt and check if they are successful
:raise AssertionError:
If the transaction failed
"""

receipt = web3.eth.get_transaction_receipt(tx_hash)

success = 0
failure = 0

for logs in receipt["logs"]:
if logs["topics"][0] == EXECUTION_FROM_MODULE_SUCCESS_TOPIC:
success += 1
elif logs["topics"][0] == EXECUTION_FROM_MODULE_FAILURE_TOPIC:
failure += 1

if success == 0 and failure == 0:
raise AssertionError(f"Does not look like a Gnosis Safe transction")
elif success + failure > 1:
raise AssertionError(f"Too many success and failures in tx. Some weird nested case?")
elif failure == 1:
raise AssertionError(f"Gnosis Safe tx failed")
elif success == 1:
return
else:
raise RuntimeError("Should not happen")
9 changes: 9 additions & 0 deletions eth_defi/uniswap_v2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"factory": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"router": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
},

# https://docs.uniswap.org/contracts/v2/reference/smart-contracts/v2-deployments
"base": {
"factory": "0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6",
"router": "0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24",
"init_code_hash": "96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
}
}

QUICKSWAP_DEPLOYMENTS = {
Expand All @@ -11,3 +18,5 @@
"router": "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff",
},
}


Loading

0 comments on commit 5961def

Please sign in to comment.