From 3c4cbe4d4e70fd2fa06017511a32b305da608ffd Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Tue, 28 Jan 2025 00:12:11 +0700 Subject: [PATCH 1/2] Add support for updating Lagoon vault fees --- eth_defi/lagoon/deployment.py | 78 ++++++++++++++++++++-------- eth_defi/lagoon/vault.py | 20 +------ scripts/lagoon/update-lagoon-fees.py | 62 ++++++++++++++++++++++ 3 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 scripts/lagoon/update-lagoon-fees.py diff --git a/eth_defi/lagoon/deployment.py b/eth_defi/lagoon/deployment.py index 79e2a9a5..d8eba2fe 100644 --- a/eth_defi/lagoon/deployment.py +++ b/eth_defi/lagoon/deployment.py @@ -10,6 +10,7 @@ Any Safe must be deployed as 1-of-1 deployer address multisig and multisig holders changed after the deployment. """ + import logging import os import time @@ -55,9 +56,11 @@ CONTRACTS_ROOT = Path(os.path.dirname(__file__)) / ".." / ".." / "contracts" + @dataclass(slots=True) class LagoonDeploymentParameters: """Capture core parameters needed to deploy a Lagoon vault""" + underlying: HexAddress name: str symbol: str @@ -92,6 +95,7 @@ class LagoonAutomatedDeployment: - Have the deployment report for the users for diagnostics """ + chain_id: int vault: LagoonVault trading_strategy_module: Contract @@ -130,7 +134,7 @@ def pformat(self) -> str: fields = self.get_deployment_data() # https://stackoverflow.com/a/17330263/315168 io = StringIO() - print("{:<30} {:30}".format('Key', 'Label'), file=io) + print("{:<30} {:30}".format("Key", "Label"), file=io) for k, v in fields.items(): print("{:<30} {:<30}".format(k, v), file=io) @@ -148,7 +152,7 @@ def deploy_lagoon( etherscan_api_key: str = None, use_forge=False, beacon_proxy=True, - beacon_address = "0x652716FaD571f04D26a3c8fFd9E593F17123Ab20" + beacon_address="0x652716FaD571f04D26a3c8fFd9E593F17123Ab20", ) -> Contract: """Deploy a new Lagoon vault. @@ -226,10 +230,7 @@ def deploy_lagoon( init_struct = parameters.as_solidity_struct() - logger.info( - "Parameters are:\n%s", - pformat(init_struct) - ) + logger.info("Parameters are:\n%s", pformat(init_struct)) # TODO: Beacon proxy deployment does not work @@ -252,11 +253,13 @@ def deploy_lagoon( False, ) - tx_params = vault.functions.initialize(init_struct).build_transaction({ - "gas": 2_000_000, - "chainId": chain_id, - "nonce": web3.eth.get_transaction_count(deployer.address), - }) + tx_params = vault.functions.initialize(init_struct).build_transaction( + { + "gas": 2_000_000, + "chainId": chain_id, + "nonce": web3.eth.get_transaction_count(deployer.address), + } + ) signed_tx = deployer.sign_transaction(tx_params) tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction) assert_transaction_success_with_explanation(web3, tx_hash) @@ -339,9 +342,7 @@ def deploy_safe_trading_strategy_module( # Enable TradingStrategyModuleV0 as Safe module # Multisig owners can enable the module - tx = safe.contract.functions.enableModule(module.address).build_transaction( - {"from": deployer.address, "gas": 0, "gasPrice": 0} - ) + tx = safe.contract.functions.enableModule(module.address).build_transaction({"from": deployer.address, "gas": 0, "gasPrice": 0}) safe_tx = safe.build_multisig_tx(safe.address, 0, tx["data"]) safe_tx.sign(deployer._private_key.hex()) tx_hash, tx = safe_tx.execute( @@ -542,11 +543,13 @@ def _broadcast(bound_func: ContractFunction): # 2. USDC.approve() for redemptions on Safe underlying = fetch_erc20_details(web3, parameters.underlying, chain_id=chain_id) - tx_data = underlying.contract.functions.approve(vault_contract.address, 2**256-1).build_transaction({ - "from": deployer.address, - "gas": 0, - "gasPrice": 0, - }) + tx_data = underlying.contract.functions.approve(vault_contract.address, 2**256 - 1).build_transaction( + { + "from": deployer.address, + "gas": 0, + "gasPrice": 0, + } + ) safe_tx = safe.build_multisig_tx(underlying.address, 0, tx_data["data"]) safe_tx.sign(deployer_local_account._private_key.hex()) tx_hash, tx = safe_tx.execute( @@ -581,6 +584,41 @@ def _broadcast(bound_func: ContractFunction): ) +def update_lagoon_vault_fees( + web3: Web3, + *, + deployer: LocalAccount | HotWallet, + vault_spec: VaultSpec, + management_rate: int, + performance_rate: int, +) -> None: + if isinstance(deployer, HotWallet): + # Production nonce hack + deployer_local_account = deployer.account + else: + deployer_local_account = deployer + + vault = LagoonVault(web3, vault_spec) + safe = vault.safe + + print(f"Updating fees: {management_rate} {performance_rate}") + + tx_data = vault.vault_contract.functions.updateRates((management_rate, performance_rate)).build_transaction( + { + "from": deployer_local_account.address, + "gas": 0, + "gasPrice": 0, + } + ) + + safe_tx = safe.build_multisig_tx(vault.vault_address, 0, tx_data["data"]) + safe_tx.sign(deployer_local_account._private_key.hex()) + tx_hash, tx = safe_tx.execute( + tx_sender_private_key=deployer_local_account._private_key.hex(), + ) + assert_transaction_success_with_explanation(web3, tx_hash) + + # https://github.com/hopperlabsxyz/lagoon-v0 LAGOON_BEACONS = { # Base @@ -593,5 +631,3 @@ def _broadcast(bound_func: ContractFunction): # Base 8453: "0x6dA4D1859bA1d02D095D2246142CdAd52233e27C", } - - diff --git a/eth_defi/lagoon/vault.py b/eth_defi/lagoon/vault.py index 36397f37..c757f915 100644 --- a/eth_defi/lagoon/vault.py +++ b/eth_defi/lagoon/vault.py @@ -147,15 +147,6 @@ def name(self) -> str: def symbol(self) -> str: return self.share_token.symbol - @cached_property - def vault_contract(self) -> Contract: - """Get vault deployment.""" - return get_deployed_contract( - self.web3, - "lagoon/Vault.json", - self.spec.vault_address, - ) - @cached_property def vault_contract(self) -> Contract: """Underlying Vault smart contract.""" @@ -206,7 +197,7 @@ def fetch_info(self) -> LagoonVaultInfo: See :py:class:`LagoonVaultInfo` """ vault_info = self.fetch_vault_info() - safe = self.fetch_safe(vault_info['safe']) + safe = self.fetch_safe(vault_info["safe"]) safe_info_dict = asdict(safe.retrieve_all_info()) del safe_info_dict["address"] # Key conflict return vault_info | safe_info_dict @@ -235,7 +226,6 @@ def fetch_total_assets(self, block_identifier: BlockIdentifier) -> Decimal: raw_amount = self.vault_contract.functions.totalAssets().call(block_identifier=block_identifier) return self.underlying_token.convert_to_decimals(raw_amount) - def fetch_total_supply(self, block_identifier: BlockIdentifier) -> Decimal: """What is the current outstanding shares. @@ -634,11 +624,3 @@ def calculate_underlying_neeeded_for_redemptions(self, block_identifier: BlockId shares_pending = self.fetch_pending_redemption(block_identifier) share_price = self.vault.fetch_share_price(block_identifier) return shares_pending * share_price - - - - - - - - diff --git a/scripts/lagoon/update-lagoon-fees.py b/scripts/lagoon/update-lagoon-fees.py new file mode 100644 index 00000000..f2f20729 --- /dev/null +++ b/scripts/lagoon/update-lagoon-fees.py @@ -0,0 +1,62 @@ +"""An example script to update a Lagoon vault's fees. + +To run: + +.. code-block:: shell + + export PRIVATE_KEY=... + export JSON_RPC_BASE=... + SIMULATE=true python scripts/lagoon/update-lagoon-fees.py +""" + +import logging +import os +import sys + +from eth_defi.hotwallet import HotWallet +from eth_defi.lagoon.vault import VaultSpec +from eth_defi.lagoon.deployment import update_lagoon_vault_fees +from eth_defi.provider.anvil import fork_network_anvil +from eth_defi.provider.multi_provider import create_multi_provider_web3 + + +logging.basicConfig(level=logging.INFO, stream=sys.stdout) + + +def main(): + PRIVATE_KEY = os.environ["PRIVATE_KEY"] + JSON_RPC_BASE = os.environ["JSON_RPC_BASE"] + VAULT_ADDRESS = os.environ["VAULT_ADDRESS"] + + MANAGEMENT_RATE = int(os.environ.get("MANAGEMENT_RATE", 0)) + PERFORMANCE_RATE = int(os.environ.get("PERFORMANCE_RATE", 0)) + + SIMULATE = os.environ.get("SIMULATE") + + deployer_wallet = HotWallet.from_private_key(PRIVATE_KEY) + + print(deployer_wallet.address) + + if SIMULATE: + print("Simulation deployment with Anvil") + anvil = fork_network_anvil(JSON_RPC_BASE) + web3 = create_multi_provider_web3(anvil.json_rpc_url) + else: + print("Base production deployment") + web3 = create_multi_provider_web3(JSON_RPC_BASE) + + chain_id = web3.eth.chain_id + + update_lagoon_vault_fees( + web3=web3, + deployer=deployer_wallet, + vault_spec=VaultSpec(chain_id, VAULT_ADDRESS), + management_rate=MANAGEMENT_RATE, + performance_rate=PERFORMANCE_RATE, + ) + + print("Lagoon vault fees updated") + + +if __name__ == "__main__": + main() From 14a2f28d79bfb09ce1b587f06d10693ddd581a08 Mon Sep 17 00:00:00 2001 From: Hieu Nguyen Date: Mon, 3 Feb 2025 18:23:09 +0700 Subject: [PATCH 2/2] Update script to propose tx instead --- eth_defi/lagoon/deployment.py | 57 +++++++++++++++++----------- scripts/lagoon/update-lagoon-fees.py | 20 +++------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/eth_defi/lagoon/deployment.py b/eth_defi/lagoon/deployment.py index d8eba2fe..a112701c 100644 --- a/eth_defi/lagoon/deployment.py +++ b/eth_defi/lagoon/deployment.py @@ -24,6 +24,7 @@ from eth_typing import HexAddress, BlockNumber from hexbytes import HexBytes from safe_eth.safe.safe import Safe +from safe_eth.safe.api.transaction_service_api.transaction_service_api import TransactionServiceApi from web3 import Web3 from web3.contract import Contract @@ -44,16 +45,27 @@ from eth_defi.uniswap_v3.deployment import UniswapV3Deployment from eth_defi.vault.base import VaultSpec - logger = logging.getLogger(__name__) +# https://github.com/hopperlabsxyz/lagoon-v0 +LAGOON_BEACONS = { + # Base + 8453: "0xD69BC314bdaa329EB18F36E4897D96A3A48C3eeF", +} + + +# https://github.com/hopperlabsxyz/lagoon-v0 +LAGOON_FEE_REGISTRIES = { + # Base + 8453: "0x6dA4D1859bA1d02D095D2246142CdAd52233e27C", +} + DEFAULT_RATE_UPDATE_COOLDOWN = 86400 DEFAULT_MANAGEMENT_RATE = 200 DEFAULT_PERFORMANCE_RATE = 2000 - CONTRACTS_ROOT = Path(os.path.dirname(__file__)) / ".." / ".." / "contracts" @@ -450,7 +462,7 @@ def deploy_automated_lagoon_vault( Deployer account must be manually removed from the Safe by new owners. """ - assert len(safe_owners) >= 1, "Multisig owners emptty" + assert len(safe_owners) >= 1, "Multisig owners empty" chain_id = web3.eth.chain_id @@ -592,6 +604,23 @@ def update_lagoon_vault_fees( management_rate: int, performance_rate: int, ) -> None: + """ + Update the management and performance fees for the Lagoon vault. + + NOTE: this function only proposes a tx to the Safe, the tx must be confirmed by the Safe owners. + + :param deployer: + The deployer account + + :param vault_spec: + The vault specification + + :param management_rate: + The management fee in BPS + + :param performance_rate: + The performance fee in BPS + """ if isinstance(deployer, HotWallet): # Production nonce hack deployer_local_account = deployer.account @@ -601,8 +630,6 @@ def update_lagoon_vault_fees( vault = LagoonVault(web3, vault_spec) safe = vault.safe - print(f"Updating fees: {management_rate} {performance_rate}") - tx_data = vault.vault_contract.functions.updateRates((management_rate, performance_rate)).build_transaction( { "from": deployer_local_account.address, @@ -613,21 +640,7 @@ def update_lagoon_vault_fees( safe_tx = safe.build_multisig_tx(vault.vault_address, 0, tx_data["data"]) safe_tx.sign(deployer_local_account._private_key.hex()) - tx_hash, tx = safe_tx.execute( - tx_sender_private_key=deployer_local_account._private_key.hex(), - ) - assert_transaction_success_with_explanation(web3, tx_hash) - - -# https://github.com/hopperlabsxyz/lagoon-v0 -LAGOON_BEACONS = { - # Base - 8453: "0xD69BC314bdaa329EB18F36E4897D96A3A48C3eeF", -} - -# https://github.com/hopperlabsxyz/lagoon-v0 -LAGOON_FEE_REGISTRIES = { - # Base - 8453: "0x6dA4D1859bA1d02D095D2246142CdAd52233e27C", -} + # setup transaction service API and propose the tx to Safe + api = TransactionServiceApi.from_ethereum_client(safe.ethereum_client) + api.post_transaction(safe_tx) diff --git a/scripts/lagoon/update-lagoon-fees.py b/scripts/lagoon/update-lagoon-fees.py index f2f20729..5ff27b9e 100644 --- a/scripts/lagoon/update-lagoon-fees.py +++ b/scripts/lagoon/update-lagoon-fees.py @@ -6,7 +6,10 @@ export PRIVATE_KEY=... export JSON_RPC_BASE=... - SIMULATE=true python scripts/lagoon/update-lagoon-fees.py + export VAULT_ADDRESS=... + export MANAGEMENT_RATE=... + export PERFORMANCE_RATE=... + python scripts/lagoon/update-lagoon-fees.py """ import logging @@ -31,20 +34,9 @@ def main(): MANAGEMENT_RATE = int(os.environ.get("MANAGEMENT_RATE", 0)) PERFORMANCE_RATE = int(os.environ.get("PERFORMANCE_RATE", 0)) - SIMULATE = os.environ.get("SIMULATE") - deployer_wallet = HotWallet.from_private_key(PRIVATE_KEY) - print(deployer_wallet.address) - - if SIMULATE: - print("Simulation deployment with Anvil") - anvil = fork_network_anvil(JSON_RPC_BASE) - web3 = create_multi_provider_web3(anvil.json_rpc_url) - else: - print("Base production deployment") - web3 = create_multi_provider_web3(JSON_RPC_BASE) - + web3 = create_multi_provider_web3(JSON_RPC_BASE) chain_id = web3.eth.chain_id update_lagoon_vault_fees( @@ -55,7 +47,7 @@ def main(): performance_rate=PERFORMANCE_RATE, ) - print("Lagoon vault fees updated") + print(f"Lagoon vault fees proposed to: management {MANAGEMENT_RATE} & performance {PERFORMANCE_RATE}, please confirm the tx on Safe") if __name__ == "__main__":