Skip to content

Commit

Permalink
Detect Safe contract addresses using configured RPC
Browse files Browse the repository at this point in the history
- When using the safe_creator use `L2 Safe` version always unless the network is `mainnet`
- Allow L2 Safes updating (previously only not L2 Safes were supported)
  • Loading branch information
Uxio0 committed Mar 14, 2023
1 parent 669ce34 commit 09e192a
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 33 deletions.
43 changes: 35 additions & 8 deletions safe_cli/operators/safe_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ens import ENS
from eth_account import Account
from eth_account.signers.local import LocalAccount
from eth_typing import ChecksumAddress
from eth_utils import ValidationError
from hexbytes import HexBytes
from packaging import version as semantic_version
Expand All @@ -28,7 +29,11 @@
from safe_cli.api.relay_service_api import RelayServiceApi
from safe_cli.api.transaction_service_api import TransactionServiceApi
from safe_cli.ethereum_hd_wallet import get_account_from_words
from safe_cli.safe_addresses import LAST_DEFAULT_CALLBACK_HANDLER, LAST_SAFE_CONTRACT
from safe_cli.safe_addresses import (
get_default_fallback_handler_address,
get_safe_contract_address,
get_safe_l2_contract_address,
)
from safe_cli.utils import get_erc_20_list, yes_or_no_question


Expand Down Expand Up @@ -201,6 +206,23 @@ def __init__(self, address: str, node_url: str):
True # Require all signatures to be present to send a tx
)

@cached_property
def last_default_fallback_handler_address(self) -> ChecksumAddress:
"""
:return: Address for last version of default fallback handler contract
"""
return get_default_fallback_handler_address(self.ethereum_client)

@cached_property
def last_safe_contract_address_address(self) -> ChecksumAddress:
"""
:return: Last version of the Safe Contract. Use events version for every network but mainnet
"""
if self.network == EthereumNetwork.MAINNET:
return get_safe_contract_address(self.ethereum_client)
else:
return get_safe_l2_contract_address(self.ethereum_client)

@cached_property
def ens_domain(self) -> Optional[str]:
# FIXME After web3.py fixes the middleware copy
Expand All @@ -218,11 +240,11 @@ def is_version_updated(self) -> bool:
:return: True if Safe Master Copy is updated, False otherwise
"""

if self._safe_cli_info.master_copy == LAST_SAFE_CONTRACT:
if self._safe_cli_info.master_copy == self.last_safe_contract_address:
return True
else: # Check versions, maybe safe-cli addresses were not updated
safe_contract = get_safe_contract(
self.ethereum_client.w3, LAST_SAFE_CONTRACT
self.ethereum_client.w3, self.last_safe_contract_address
)
try:
safe_contract_version = safe_contract.functions.VERSION().call()
Expand Down Expand Up @@ -515,7 +537,10 @@ def update_version(self) -> Optional[bool]:
if self.is_version_updated():
raise SafeAlreadyUpdatedException()

addresses = (LAST_SAFE_CONTRACT, LAST_DEFAULT_CALLBACK_HANDLER)
addresses = (
self.last_safe_contract_address,
self.last_default_fallback_handler_address,
)
if not all(
self.ethereum_client.is_contract(contract) for contract in addresses
):
Expand All @@ -529,10 +554,10 @@ def update_version(self) -> Optional[bool]:
MultiSendTx(MultiSendOperation.CALL, self.address, 0, data)
for data in (
self.safe_contract_1_1_0.functions.changeMasterCopy(
LAST_SAFE_CONTRACT
self.last_safe_contract_address
).build_transaction(tx_params)["data"],
self.safe_contract_1_1_0.functions.setFallbackHandler(
LAST_DEFAULT_CALLBACK_HANDLER
self.last_default_fallback_handler_address
).build_transaction(tx_params)["data"],
)
]
Expand All @@ -542,8 +567,10 @@ def update_version(self) -> Optional[bool]:
if self.prepare_and_execute_safe_transaction(
multisend.address, 0, multisend_data, operation=SafeOperation.DELEGATE_CALL
):
self.safe_cli_info.master_copy = LAST_SAFE_CONTRACT
self.safe_cli_info.fallback_handler = LAST_DEFAULT_CALLBACK_HANDLER
self.safe_cli_info.master_copy = self.last_safe_contract_address
self.safe_cli_info.fallback_handler = (
self.last_default_fallback_handler_address
)
self.safe_cli_info.version = self.safe.retrieve_version()

def change_threshold(self, threshold: int):
Expand Down
94 changes: 87 additions & 7 deletions safe_cli/safe_addresses.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,87 @@
# https://github.com/gnosis/safe-deployments/tree/main/src/assets/v1.3.0
LAST_SAFE_CONTRACT = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"
LAST_SAFE_L2_CONTRACT = "0x3E5c63644E683549055b9Be8653de26E0B4CD36E"
LAST_DEFAULT_CALLBACK_HANDLER = "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4"
LAST_PROXY_FACTORY_CONTRACT = "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2"
LAST_MULTISEND_CONTRACT = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"
LAST_MULTISEND_CALL_ONLY_CONTRACT = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
"""
Get the correct addresses for the contracts by testing the deployment addresses using the RPC
Currently using Safe v1.3.0
https://github.com/gnosis/safe-deployments/tree/main/src/assets/v1.3.0
"""
from eth_typing import ChecksumAddress

from gnosis.eth import EthereumClient


def _get_valid_contract(
ethereum_client: EthereumClient, addresses: ChecksumAddress
) -> ChecksumAddress:
"""
:param ethereum_client:
:param addresses:
:return: First valid contract found in blockchain
"""

for address in addresses:
if ethereum_client.is_contract(address):
return address
raise ValueError(f"Network ${ethereum_client.get_network()} is not supported")


def get_safe_contract_address(ethereum_client: EthereumClient) -> ChecksumAddress:
return _get_valid_contract(
ethereum_client,
[
"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552",
"0x69f4D1788e39c87893C980c06EdF4b7f686e2938",
],
)


def get_safe_l2_contract_address(ethereum_client: EthereumClient) -> ChecksumAddress:
return _get_valid_contract(
ethereum_client,
[
"0x3E5c63644E683549055b9Be8653de26E0B4CD36E",
"0xfb1bffC9d739B8D520DaF37dF666da4C687191EA",
],
)


def get_default_fallback_handler_address(
ethereum_client: EthereumClient,
) -> ChecksumAddress:
return _get_valid_contract(
ethereum_client,
[
"0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
"0x017062a1dE2FE6b99BE3d9d37841FeD19F573804",
],
)


def get_proxy_factory_address(ethereum_client: EthereumClient) -> ChecksumAddress:
return _get_valid_contract(
ethereum_client,
[
"0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
"0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC",
],
)


def get_last_multisend_address(ethereum_client: EthereumClient) -> ChecksumAddress:
return _get_valid_contract(
ethereum_client,
[
"0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
"0x998739BFdAAdde7C933B942a68053933098f9EDa",
],
)


def get_last_multisend_call_only_address(
ethereum_client: EthereumClient,
) -> ChecksumAddress:
return _get_valid_contract(
ethereum_client,
[
"0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
"0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B"
],
)
41 changes: 23 additions & 18 deletions safe_cli/safe_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@

from safe_cli.prompt_parser import check_ethereum_address
from safe_cli.safe_addresses import (
LAST_DEFAULT_CALLBACK_HANDLER,
LAST_PROXY_FACTORY_CONTRACT,
LAST_SAFE_CONTRACT,
LAST_SAFE_L2_CONTRACT,
get_default_fallback_handler_address,
get_proxy_factory_address,
get_safe_contract_address,
get_safe_l2_contract_address,
)
from safe_cli.utils import yes_or_no_question

Expand Down Expand Up @@ -77,14 +77,14 @@ def setup_argument_parser():
parser.add_argument(
"--proxy-factory",
help="Use a custom proxy factory",
default=LAST_PROXY_FACTORY_CONTRACT,
default=None,
type=check_ethereum_address,
)
parser.add_argument(
"--callback-handler",
help="Use a custom fallback handler. It is not required for Safe Master Copies "
"with version < 1.1.0",
default=LAST_DEFAULT_CALLBACK_HANDLER,
default=None,
type=check_ethereum_address,
)
parser.add_argument(
Expand All @@ -98,8 +98,8 @@ def setup_argument_parser():
)

parser.add_argument(
"--l2",
help="Use L2 deployment of the Safe instead of the regular one. Recommended for every network but mainnet",
"--without-events",
help="Use non events deployment of the Safe instead of the regular one. Recommended for mainnet to save gas costs when using the Safe",
default=False,
action="store_true",
)
Expand All @@ -108,9 +108,7 @@ def setup_argument_parser():

def main(*args, **kwargs):
parser = setup_argument_parser()
print_formatted_text(
pyfiglet.figlet_format("Gnosis Safe Creator")
) # Print fancy text
print_formatted_text(pyfiglet.figlet_format("Safe Creator")) # Print fancy text
args = parser.parse_args()
node_url: URI = args.node_url
account: LocalAccount = Account.from_key(args.private_key)
Expand All @@ -129,14 +127,21 @@ def main(*args, **kwargs):
)
sys.exit(1)

safe_contract_address = args.safe_contract or (
LAST_SAFE_L2_CONTRACT if args.l2 else LAST_SAFE_CONTRACT
)
proxy_factory_address = args.proxy_factory
fallback_handler = args.callback_handler
ethereum_client = EthereumClient(node_url)
ethereum_network = ethereum_client.get_network()

safe_contract_address = args.safe_contract or (
get_safe_contract_address(ethereum_client)
if args.without_events
else get_safe_l2_contract_address(ethereum_client)
)
proxy_factory_address = args.proxy_factory or get_proxy_factory_address(
ethereum_client
)
fallback_handler = args.callback_handler or get_default_fallback_handler_address(
ethereum_client
)

if not ethereum_client.is_contract(safe_contract_address):
print_formatted_text(
f"Safe contract address {safe_contract_address} "
Expand Down Expand Up @@ -182,8 +187,8 @@ def main(*args, **kwargs):
f"Creating new Safe with owners={owners} threshold={threshold} salt-nonce={salt_nonce}"
)
print_formatted_text(
f"Proxy factory={proxy_factory_address} safe-master-copy={safe_contract_address} and "
f"fallback-handler={fallback_handler}"
f"Safe-master-copy={safe_contract_address}\nFallback-handler={fallback_handler}\n"
f"Proxy factory={proxy_factory_address}"
)
if yes_or_no_question("Do you want to continue?"):
safe_contract = get_safe_V1_3_0_contract(
Expand Down

0 comments on commit 09e192a

Please sign in to comment.