Skip to content

Commit

Permalink
Work around nonce sync issues in Lagoon deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
miohtama committed Jan 8, 2025
1 parent 9435440 commit c9fd954
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 22 deletions.
40 changes: 40 additions & 0 deletions eth_defi/hotwallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,12 +369,52 @@ def transact_with_contract(
assert func.address is not None, f"ContractFunction is not bound to a contract instance: {func}"
web3 = func.w3
assert web3 is not None, "ContractFunction not bound to web3 instance"

tx_data = func(*args, **kwargs).build_transaction({
"from": self.address,
})

self.fill_in_gas_price(web3, tx_data)
return self.sign_transaction_with_new_nonce(tx_data)

def transact_and_broadcast_with_contract(
self,
func: ContractFunction,
gas_limit: int=None,
) -> HexBytes:
"""Transacts with a contract, broadcasts transaction.
- Shorthand method
- Build a transaction and signs it
- Always use a correct manually managed nonce
Example:
.. code-block:: python
bound_func = module.functions.whitelistUniswapV3Router(uniswap_v3.swap_router.address, "Allow Uniswap v3")
tx_hash = deployer.transact_and_broadcast_with_contract(bound_func)
:return:
Transaction hash
"""
assert isinstance(func, ContractFunction), f"Got: {type(func)}"
assert func.args is not None, f"Unbound contract function? {func}"
web3 = func.w3

tx_data = func.build_transaction({
"from": self.address,
})

if gas_limit is not None:
tx_data["gas"] = gas_limit

self.fill_in_gas_price(web3, tx_data)
signed_tx = self.sign_transaction_with_new_nonce(tx_data)

tx_hash = web3.eth.send_raw_transaction(signed_tx.raw_transaction)
return tx_hash

@staticmethod
def fill_in_gas_price(web3: Web3, tx: dict) -> dict:
"""Fills in the gas value fields for a transaction.
Expand Down
73 changes: 57 additions & 16 deletions eth_defi/lagoon/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
from io import StringIO
from pathlib import Path
from pprint import pformat
from typing import Callable

from eth_account.signers.local import LocalAccount
from eth_typing import HexAddress, BlockNumber
from hexbytes import HexBytes
from safe_eth.safe.safe import Safe

from web3 import Web3
from web3.contract import Contract
from web3.contract.contract import ContractFunction

from eth_defi.abi import get_contract
from eth_defi.deploy import deploy_contract
from eth_defi.foundry.forge import deploy_contract_with_forge
from eth_defi.hotwallet import HotWallet
from eth_defi.lagoon.beacon_proxy import deploy_beacon_proxy
from eth_defi.lagoon.vault import LagoonVault
from eth_defi.middleware import construct_sign_and_send_raw_middleware_anvil
Expand Down Expand Up @@ -356,57 +360,62 @@ def setup_guard(
asset_manager: HexAddress,
vault: Contract,
module: Contract,
broadcast_func: Callable[[ContractFunction], HexBytes],
any_asset: bool = False,
uniswap_v2: UniswapV2Deployment = None,
uniswap_v3: UniswapV3Deployment = None,
):

assert isinstance(deployer, LocalAccount)
assert isinstance(deployer, HotWallet)
assert type(owner) == str
assert isinstance(module, Contract)
assert isinstance(vault, Contract)
assert any_asset, f"Only wildcard trading supported at the moment"
assert callable(broadcast_func), "Must have a broadcast function for txs"

web3.middleware_onion.add(construct_sign_and_send_raw_middleware_anvil(deployer))
_broadcast = broadcast_func

logger.info("Setting up TradingStrategyModuleV0 guard")

# Enable asset_manager as the whitelisted trade-executor
tx_hash = module.functions.allowSender(asset_manager, "Whitelist trade-executor").transact({"from": deployer.address})
logger.info("Whitelisting trade-executor as sender")
tx_hash = _broadcast(module.functions.allowSender(asset_manager, "Whitelist trade-executor"))
assert_transaction_success_with_explanation(web3, tx_hash)

# Enable safe as the receiver of tokens
tx_hash = module.functions.allowReceiver(safe.address, "Whitelist Safe as trade receiver").transact({"from": deployer.address})
logger.info("Whitelist Safe as trade receiver")
tx_hash = _broadcast(module.functions.allowReceiver(safe.address, "Whitelist Safe as trade receiver"))
assert_transaction_success_with_explanation(web3, tx_hash)

# Whitelist Uniswap v2
if uniswap_v2:
logger.info("Whitelisting Uniswap v2 router: %s", uniswap_v2.router.address)
tx_hash = module.functions.whitelistUniswapV2Router(uniswap_v2.router.address, "Allow Uniswap v2").transact({"from": deployer.address})
tx_hash = _broadcast(module.functions.whitelistUniswapV2Router(uniswap_v2.router.address, "Allow Uniswap v2"))
assert_transaction_success_with_explanation(web3, tx_hash)

# Whitelist Uniswap v3
if uniswap_v3:
logger.info("Whitelisting Uniswap v3 router: %s", uniswap_v3.swap_router.address)
tx_hash = module.functions.whitelistUniswapV3Router(uniswap_v3.swap_router.address, "Allow Uniswap v3").transact({"from": deployer.address})
tx_hash = _broadcast(module.functions.whitelistUniswapV3Router(uniswap_v3.swap_router.address, "Allow Uniswap v3"))
assert_transaction_success_with_explanation(web3, tx_hash)

# Whitelist all assets
if any_asset:
logger.info("Allow any asset whitelist")
tx_hash = module.functions.setAnyAssetAllowed(True, "Allow any asset").transact({"from": deployer.address})
tx_hash = _broadcast(module.functions.setAnyAssetAllowed(True, "Allow any asset"))
assert_transaction_success_with_explanation(web3, tx_hash)
else:
logger.info("Using only whitelisted assets")

# Whitelist vault settle
tx_hash = module.functions.whitelistLagoon(vault.address, "Whitelist vault settlement").transact({"from": deployer.address})
logger.info("Whitelist vault settlement")
tx_hash = _broadcast(module.functions.whitelistLagoon(vault.address, "Whitelist vault settlement"))
assert_transaction_success_with_explanation(web3, tx_hash)


def deploy_automated_lagoon_vault(
web3: Web3,
deployer: LocalAccount,
deployer: LocalAccount | HotWallet,
asset_manager: HexAddress,
parameters: LagoonDeploymentParameters,
safe_owners: list[HexAddress | str],
Expand All @@ -431,6 +440,10 @@ def deploy_automated_lagoon_vault(
- Asset manager (Trading Straegy) and Valuation Manager (Lagoon) are the same role
- Any Safe must be deployed as 1-of-1 deployer address multisig and multisig holders changed after the deployment.
.. warning::
Because we need to mix Forge, Safe lib and Web3.py transaction nonce management becomes a madness.
.. note ::
Deployer account must be manually removed from the Safe by new owners.
Expand All @@ -440,9 +453,29 @@ def deploy_automated_lagoon_vault(

chain_id = web3.eth.chain_id

if isinstance(deployer, HotWallet):
# Production nonce hack
deployer_local_account = deployer.account
else:
deployer_local_account = deployer

# Hack together a nonce management helper
def _broadcast(bound_func: ContractFunction):
assert isinstance(bound_func, ContractFunction)
assert bound_func.args is not None
if isinstance(deployer, HotWallet):
# Path must be taken with prod deployers
return deployer.transact_and_broadcast_with_contract(bound_func)
elif isinstance(deployer, LocalAccount):
# Only for Anvil
# Will cause nonce sync errors in proc
return bound_func.transact({"from": deployer.address})
else:
raise NotImplementedError(f"No idea about: {deployer}")

safe = deploy_safe(
web3,
deployer,
deployer_local_account,
owners=[deployer.address],
threshold=1,
)
Expand All @@ -455,7 +488,7 @@ def deploy_automated_lagoon_vault(

vault_contract = deploy_lagoon(
web3=web3,
deployer=deployer,
deployer=deployer_local_account,
safe=safe,
asset_manager=asset_manager,
parameters=parameters,
Expand All @@ -470,12 +503,19 @@ def deploy_automated_lagoon_vault(

module = deploy_safe_trading_strategy_module(
web3=web3,
deployer=deployer,
deployer=deployer_local_account,
safe=safe,
etherscan_api_key=etherscan_api_key,
use_forge=use_forge,
)

if not is_anvil(web3):
logger.info("Between contracts deployment delay: Sleeping %s for new nonce to propagade", between_contracts_delay_seconds)
time.sleep(between_contracts_delay_seconds)

if isinstance(deployer, HotWallet):
deployer.sync_nonce(web3)

setup_guard(
web3=web3,
safe=safe,
Expand All @@ -487,6 +527,7 @@ def deploy_automated_lagoon_vault(
uniswap_v2=uniswap_v2,
uniswap_v3=uniswap_v3,
any_asset=any_asset,
broadcast_func=_broadcast,
)

# After everything is deployed, fix ownership
Expand All @@ -496,7 +537,7 @@ def deploy_automated_lagoon_vault(

# 1. Transfer guard ownership to Gnosis
assert module.functions.owner().call() == deployer.address
tx_hash = module.functions.transferOwnership(safe.address).transact({"from": deployer.address})
tx_hash = _broadcast(module.functions.transferOwnership(safe.address))
assert_transaction_success_with_explanation(web3, tx_hash)

# 2. USDC.approve() for redemptions on Safe
Expand All @@ -507,9 +548,9 @@ def deploy_automated_lagoon_vault(
"gasPrice": 0,
})
safe_tx = safe.build_multisig_tx(underlying.address, 0, tx_data["data"])
safe_tx.sign(deployer._private_key.hex())
safe_tx.sign(deployer_local_account._private_key.hex())
tx_hash, tx = safe_tx.execute(
tx_sender_private_key=deployer._private_key.hex(),
tx_sender_private_key=deployer_local_account._private_key.hex(),
)
assert_transaction_success_with_explanation(web3, tx_hash)

Expand All @@ -518,7 +559,7 @@ def deploy_automated_lagoon_vault(
add_new_safe_owners(
web3,
safe,
deployer,
deployer_local_account,
owners=safe_owners,
threshold=safe_threshold,
)
Expand Down
7 changes: 7 additions & 0 deletions eth_defi/safe/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ def add_new_safe_owners(
:param gas_per_tx:
Gas limit for a single transaction.
:param between_calls_sleep:
Deployer hack
More info:
- https://github.com/safe-global/safe-smart-account/blob/main/contracts/base/OwnerManager.sol#L56C14-L56C35
Expand All @@ -118,6 +122,7 @@ def add_new_safe_owners(
logger.info("Deployer: already exist on Safe cosigner")
continue

logger.info("Adding owner %s", owner)
tx = safe.contract.functions.addOwnerWithThreshold(owner, 1).build_transaction(
{"from": deployer.address, "gas": gas_per_tx, "gasPrice": 0}
)
Expand All @@ -129,6 +134,7 @@ def add_new_safe_owners(
assert_transaction_success_with_explanation(web3, tx_hash)

# Change the threshold
logger.info("Changing signign threhold to: %d", threshold)
tx = safe.contract.functions.changeThreshold(threshold).build_transaction(
{"from": deployer.address, "gas": gas_per_tx, "gasPrice": 0}
)
Expand All @@ -138,3 +144,4 @@ def add_new_safe_owners(
tx_sender_private_key=deployer._private_key.hex(),
)
assert_transaction_success_with_explanation(web3, tx_hash)
logger.info("Owners updated")
11 changes: 8 additions & 3 deletions tests/lagoon/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,17 @@ def uniswap_v2(web3):
)


@pytest.fixture()
def deployer_hot_wallet(web3) -> HotWallet:
"""Manual nonce manager used for Lagoon deployment"""
hot_wallet = HotWallet.create_for_testing(web3, eth_amount=1)
return hot_wallet


@pytest.fixture()
def deployer_local_account(web3) -> LocalAccount:
def deployer_local_account(deployer_hot_wallet) -> LocalAccount:
"""Account that we use for Lagoon deployment"""
hot_wallet = HotWallet.create_for_testing(web3, eth_amount=1)
return hot_wallet.account
return deployer_hot_wallet.account


@pytest.fixture()
Expand Down
5 changes: 2 additions & 3 deletions tests/lagoon/test_deploy_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def test_lagoon_deploy_base_guarded_any_token(
topped_up_asset_manager: HexAddress,
depositor: HexAddress,
usdc_holder: HexAddress,
deployer_local_account,
deployer_hot_wallet: HotWallet,
multisig_owners: list[str],
):
"""Deploy a new automated Lagoon vault
Expand All @@ -84,7 +84,6 @@ def test_lagoon_deploy_base_guarded_any_token(
"""

chain_id = web3.eth.chain_id
deployer = deployer_local_account
asset_manager = topped_up_asset_manager
usdc = base_usdc

Expand All @@ -96,7 +95,7 @@ def test_lagoon_deploy_base_guarded_any_token(

deploy_info = deploy_automated_lagoon_vault(
web3=web3,
deployer=deployer,
deployer=deployer_hot_wallet,
asset_manager=asset_manager,
parameters=parameters,
safe_owners=multisig_owners,
Expand Down

0 comments on commit c9fd954

Please sign in to comment.