1+ from abc import ABCMeta
12import os
23import re
3- import warnings
44from copy import copy
55from typing import Dict , List , Optional , Union
6- from enum import Enum
76import click
8- from gnosis .eth import EthereumClient , EthereumNetwork
97from web3 import Web3 # don't move below brownie import
108from brownie import Contract , accounts , chain , history , web3
119from brownie .convert .datatypes import EthAddress
1210from brownie .network .account import LocalAccount
1311from brownie .network .transaction import TransactionReceipt
14- from eth_abi import encode_abi
12+ from eth_abi import encode
1513from eth_utils import is_address , to_checksum_address , encode_hex , keccak
16- from gnosis .safe import Safe , SafeOperation
14+ from gnosis .safe import Safe
15+ from gnosis .eth import EthereumClient
16+ from gnosis .safe .safe import SafeV111 , SafeV120 , SafeV130 , SafeV141
17+ from gnosis .safe .enums import SafeOperationEnum
1718from gnosis .safe .multi_send import MultiSend , MultiSendOperation , MultiSendTx
1819from gnosis .safe .safe_tx import SafeTx
1920from gnosis .safe .signatures import signature_split , signature_to_bytes
2021from gnosis .safe .api import TransactionServiceApi
21- from gnosis .eth .ethereum_client import EthereumNetworkNotSupported
2222from hexbytes import HexBytes
2323from trezorlib import ethereum , tools , ui
2424from trezorlib .client import TrezorClient
2727from functools import cached_property
2828
2929
30- class EthereumNetworkBackport (Enum ):
31- ARBITRUM_ONE = 42161
32- AURORA_MAINNET = 1313161554
33- AVALANCHE_C_CHAIN = 43114
34- BASE = 8453
35- BASE_GOERLI = 84531
36- BINANCE_SMART_CHAIN_MAINNET = 56
37- CELO = 42220
38- ENERGY_WEB_CHAIN = 246
39- GOERLI = 5
40- MAINNET = 1
41- POLYGON = 137
42- OPTIMISM = 10
43- ENERGY_WEB_VOLTA_TESTNET = 73799
44- GNOSIS = 100
45- FANTOM = 250
46- BOBA_NETWORK = 288
47-
48-
49- # MultiSendCallOnly doesn't allow delegatecalls
50- # https://github.com/safe-global/safe-deployments/blob/main/src/assets/v1.3.0/multi_send_call_only.json
51- DEFAULT_MULTISEND_CALL_ONLY = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
52- ALT_MULTISEND_CALL_ONLY = "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B"
53- CUSTOM_MULTISENDS = {
54- EthereumNetworkBackport .FANTOM : "0x10B62CC1E8D9a9f1Ad05BCC491A7984697c19f7E" ,
55- EthereumNetworkBackport .OPTIMISM : ALT_MULTISEND_CALL_ONLY ,
56- EthereumNetworkBackport .BOBA_NETWORK : ALT_MULTISEND_CALL_ONLY ,
57- EthereumNetworkBackport .BASE : ALT_MULTISEND_CALL_ONLY ,
58- EthereumNetworkBackport .CELO : ALT_MULTISEND_CALL_ONLY ,
59- EthereumNetworkBackport .AVALANCHE_C_CHAIN : ALT_MULTISEND_CALL_ONLY ,
60- EthereumNetworkBackport .BASE_GOERLI : ALT_MULTISEND_CALL_ONLY ,
61- }
62-
63- class TransactionServiceBackport (TransactionServiceApi ):
64- URL_BY_NETWORK = {
65- EthereumNetworkBackport .ARBITRUM_ONE : "https://safe-transaction-arbitrum.safe.global" ,
66- EthereumNetworkBackport .AURORA_MAINNET : "https://safe-transaction-aurora.safe.global" ,
67- EthereumNetworkBackport .AVALANCHE_C_CHAIN : "https://safe-transaction-avalanche.safe.global" ,
68- EthereumNetworkBackport .BASE : "https://safe-transaction-base.safe.global" ,
69- EthereumNetworkBackport .BASE_GOERLI : "https://safe-transaction-base-testnet.safe.global" ,
70- EthereumNetworkBackport .BINANCE_SMART_CHAIN_MAINNET : "https://safe-transaction-bsc.safe.global" ,
71- EthereumNetworkBackport .CELO : "https://safe-transaction-celo.safe.global" ,
72- EthereumNetworkBackport .ENERGY_WEB_CHAIN : "https://safe-transaction-ewc.safe.global" ,
73- EthereumNetworkBackport .GOERLI : "https://safe-transaction-goerli.safe.global" ,
74- EthereumNetworkBackport .MAINNET : "https://safe-transaction-mainnet.safe.global" ,
75- EthereumNetworkBackport .POLYGON : "https://safe-transaction-polygon.safe.global" ,
76- EthereumNetworkBackport .OPTIMISM : "https://safe-transaction-optimism.safe.global" ,
77- EthereumNetworkBackport .ENERGY_WEB_VOLTA_TESTNET : "https://safe-transaction-volta.safe.global" ,
78- EthereumNetworkBackport .GNOSIS : "https://safe-transaction-gnosis-chain.safe.global" ,
79- EthereumNetworkBackport .FANTOM : "https://safe-txservice.fantom.network" ,
80- EthereumNetworkBackport .BOBA_NETWORK : "https://safe-transaction.mainnet.boba.network" ,
81- }
82-
83- def __init__ (self , network : EthereumNetwork , ethereum_client : Optional [EthereumClient ] = None , base_url : Optional [str ] = None ):
84- self .network = network
85- self .ethereum_client = ethereum_client
86- self .base_url = base_url or self .URL_BY_NETWORK .get (EthereumNetworkBackport (network .value ))
87- if not self .base_url :
88- raise EthereumNetworkNotSupported (network )
89-
90-
91- warnings .filterwarnings ('ignore' , 'The function signature for resolver.*' )
92-
93-
9430class ExecutionFailure (Exception ):
9531 pass
9632
@@ -99,17 +35,36 @@ class ApiError(Exception):
9935 pass
10036
10137
102- class BrownieSafe (Safe ):
38+ class ContractWrapper :
39+ def __init__ (self , account , instance ):
40+ self .account = account
41+ self .instance = instance
10342
104- def __init__ (self , address , base_url = None , multisend = None ):
105- """
106- Create an BrownieSafe from an address or a ENS name and use a default connection.
107- """
108- address = to_checksum_address (address ) if is_address (address ) else web3 .ens .resolve (address )
109- ethereum_client = EthereumClient (web3 .provider .endpoint_uri )
110- self .transaction_service = TransactionServiceBackport (ethereum_client .get_network (), ethereum_client , base_url )
111- self .multisend = multisend or CUSTOM_MULTISENDS .get (EthereumNetworkBackport (chain .id ), DEFAULT_MULTISEND_CALL_ONLY )
43+ def __call__ (self , address ):
44+ address = to_address (address )
45+ return Contract (address , owner = self .account )
46+
47+ def __getattr__ (self , attr ):
48+ return getattr (self .instance , attr )
49+
50+
51+ def to_address (address ):
52+ if is_address (address ):
53+ return to_checksum_address (address )
54+ return web3 .ens .address (address )
55+
56+
57+ class BrownieSafeBase (metaclass = ABCMeta ):
58+
59+ def __init__ (self , address , ethereum_client ):
11260 super ().__init__ (address , ethereum_client )
61+
62+ # safe-eth-py shadows the .contract method after 4.3.2
63+ # we use a wrapper that satisfies both use cases
64+ # 1. web3 safe contract instance using __getattr__
65+ # 2. instantiating contract instance with safe as an owner using __call__
66+ self .contract = ContractWrapper (self .account , self .contract )
67+
11368 if self .client == 'anvil' :
11469 web3 .manager .request_blocking ('anvil_setNextBlockBaseFeePerGas' , ['0x0' ])
11570
@@ -121,7 +76,7 @@ def __repr__(self):
12176
12277 @cached_property
12378 def client (self ):
124- client_version = web3 .clientVersion
79+ client_version = web3 .client_version
12580 match = re .search ('(anvil|hardhat|ganache)' , client_version .lower ())
12681 return match .group (1 ) if match else client_version
12782
@@ -132,21 +87,14 @@ def account(self) -> LocalAccount:
13287 """
13388 return accounts .at (self .address , force = True )
13489
135- def contract (self , address ) -> Contract :
136- """
137- Instantiate a Brownie Contract owned by Safe account.
138- """
139- address = to_checksum_address (address ) if is_address (address ) else web3 .ens .resolve (address )
140- return Contract (address , owner = self .account )
141-
14290 def pending_nonce (self ) -> int :
14391 """
14492 Subsequent nonce which accounts for pending transactions in the transaction service.
14593 """
14694 results = self .transaction_service .get_transactions (self .address )
14795 return results [0 ]['nonce' ] + 1 if results else 0
14896
149- def tx_from_receipt (self , receipt : TransactionReceipt , operation : SafeOperation = SafeOperation .CALL , safe_nonce : int = None ) -> SafeTx :
97+ def tx_from_receipt (self , receipt : TransactionReceipt , operation : SafeOperationEnum = SafeOperationEnum .CALL , safe_nonce : int = None ) -> SafeTx :
15098 """
15199 Convert Brownie transaction receipt to a Safe transaction.
152100 """
@@ -166,10 +114,8 @@ def multisend_from_receipts(self, receipts: List[TransactionReceipt] = None, saf
166114 safe_nonce = self .pending_nonce ()
167115
168116 txs = [MultiSendTx (MultiSendOperation .CALL , tx .receiver , tx .value , tx .input ) for tx in receipts ]
169- data = MultiSend (
170- ethereum_client = self .ethereum_client , address = self .multisend
171- ).build_tx_data (txs )
172- return self .build_multisig_tx (self .multisend , 0 , data , SafeOperation .DELEGATE_CALL .value , safe_nonce = safe_nonce )
117+ data = self .multisend .build_tx_data (txs )
118+ return self .build_multisig_tx (self .multisend .address , 0 , data , SafeOperationEnum .DELEGATE_CALL .value , safe_nonce = safe_nonce )
173119
174120 def get_signer (self , signer : Optional [Union [LocalAccount , str ]] = None ) -> LocalAccount :
175121 if signer is None :
@@ -328,7 +274,7 @@ def estimate_gas(self, safe_tx: SafeTx) -> int:
328274 return self .estimate_tx_gas (safe_tx .to , safe_tx .value , safe_tx .data , safe_tx .operation )
329275
330276 def set_storage (self , account : str , slot : int , value : int ):
331- params = [account , hex (slot ), encode_hex (encode_abi (['uint' ], [value ]))]
277+ params = [account , hex (slot ), encode_hex (encode (['uint' ], [value ]))]
332278 method = {
333279 'anvil' : 'anvil_setStorageAt' ,
334280 'hardhat' : 'hardhat_setStorageAt' ,
@@ -338,7 +284,7 @@ def set_storage(self, account: str, slot: int, value: int):
338284
339285 def preview_tx (self , safe_tx : SafeTx , events = True , call_trace = False ) -> TransactionReceipt :
340286 tx = copy (safe_tx )
341- safe = Contract .from_abi ('Gnosis Safe' , self .address , self .get_contract () .abi )
287+ safe = Contract .from_abi ('Gnosis Safe' , self .address , self .contract .abi )
342288 # Replace pending nonce with the subsequent nonce, this could change the safe_tx_hash
343289 tx .safe_nonce = safe .nonce ()
344290 # Forge signatures from the needed amount of owners, skip the one which submits the tx
@@ -349,15 +295,15 @@ def preview_tx(self, safe_tx: SafeTx, events=True, call_trace=False) -> Transact
349295 # Signautres are encoded as [bytes32 r, bytes32 s, bytes8 v]
350296 # Pre-validated signatures are encoded as r=owner, s unused and v=1.
351297 # https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures
352- tx .signatures = b'' .join ([encode_abi (['address' , 'uint' ], [str (owner ), 0 ]) + b'\x01 ' for owner in owners ])
298+ tx .signatures = b'' .join ([encode (['address' , 'uint' ], [str (owner ), 0 ]) + b'\x01 ' for owner in owners ])
353299
354300 # approvedHashes are in slot 8 and have type of mapping(address => mapping(bytes32 => uint256))
355301 for owner in owners [:threshold ]:
356- outer_key = keccak (encode_abi (['address' , 'uint' ], [str (owner ), 8 ]))
302+ outer_key = keccak (encode (['address' , 'uint' ], [str (owner ), 8 ]))
357303 slot = int .from_bytes (keccak (tx .safe_tx_hash + outer_key ), 'big' )
358304 self .set_storage (tx .safe_address , slot , 1 )
359305
360- payload = tx .w3_tx .buildTransaction ()
306+ payload = tx .w3_tx .build_transaction ()
361307 receipt = owners [0 ].transfer (payload ['to' ], payload ['value' ], gas_limit = payload ['gas' ], data = payload ['data' ])
362308
363309 if 'ExecutionSuccess' not in receipt .events :
@@ -385,7 +331,7 @@ def execute_transaction(self, safe_tx: SafeTx, signer=None) -> TransactionReceip
385331 """
386332 Execute a fully signed transaction likely retrieved from the pending_transactions method.
387333 """
388- payload = safe_tx .w3_tx .buildTransaction ()
334+ payload = safe_tx .w3_tx .build_transaction ()
389335 signer = self .get_signer (signer )
390336 receipt = signer .transfer (payload ['to' ], payload ['value' ], gas_limit = payload ['gas' ], data = payload ['data' ])
391337 return receipt
@@ -398,13 +344,13 @@ def execute_transaction_with_frame(self, safe_tx: SafeTx, frame_rpc="http://127.
398344 frame = Web3 (Web3 .HTTPProvider (frame_rpc , {'timeout' : 600 }))
399345 account = frame .eth .accounts [0 ]
400346 frame .manager .request_blocking ('wallet_switchEthereumChain' , [{'chainId' : hex (chain .id )}])
401- payload = safe_tx .w3_tx .buildTransaction ()
347+ payload = safe_tx .w3_tx .build_transaction ()
402348 tx = {
403349 "from" : account ,
404350 "to" : self .address ,
405351 "value" : payload ["value" ],
406352 "nonce" : frame .eth .get_transaction_count (account ),
407- "gas" : web3 .toHex (payload ["gas" ]),
353+ "gas" : web3 .to_hex (payload ["gas" ]),
408354 "data" : HexBytes (payload ["data" ]),
409355 }
410356 frame .eth .send_transaction (tx )
@@ -415,3 +361,41 @@ def preview_pending(self, events=True, call_trace=False):
415361 """
416362 for safe_tx in self .pending_transactions :
417363 self .preview_tx (safe_tx , events = events , call_trace = call_trace )
364+
365+
366+ class BrownieSafeV111 (BrownieSafeBase , SafeV111 ):
367+ pass
368+
369+ class BrownieSafeV120 (BrownieSafeBase , SafeV120 ):
370+ pass
371+
372+ class BrownieSafeV130 (BrownieSafeBase , SafeV130 ):
373+ pass
374+
375+ class BrownieSafeV141 (BrownieSafeBase , SafeV141 ):
376+ pass
377+
378+
379+ PATCHED_SAFE_VERSIONS = {
380+ '1.1.1' : BrownieSafeV111 ,
381+ '1.2.0' : BrownieSafeV120 ,
382+ '1.3.0' : BrownieSafeV130 ,
383+ '1.4.1' : BrownieSafeV141 ,
384+ }
385+
386+
387+ def BrownieSafe (address , base_url = None , multisend = None ):
388+ """
389+ Create an BrownieSafe from an address or a ENS name and use a default connection.
390+ """
391+ address = to_address (address )
392+ ethereum_client = EthereumClient (web3 .provider .endpoint_uri )
393+ safe = Safe (address , ethereum_client )
394+ version = safe .get_version ()
395+
396+ brownie_safe = PATCHED_SAFE_VERSIONS [version ](address , ethereum_client )
397+ brownie_safe .transaction_service = TransactionServiceApi (ethereum_client .get_network (), ethereum_client , base_url )
398+ brownie_safe .multisend = MultiSend (ethereum_client , multisend , call_only = True )
399+
400+ return brownie_safe
401+
0 commit comments