Skip to content

Commit 3e54476

Browse files
authored
feat: update (#65)
* chore: bump deps * chore: ignore scripts * chore: update poetry.lock * fix: eth-abi changes * fix: safe-eth-py changes * refactor: remove transaction service backport * fix: override hacky factory * feat: contract wrapper * chore: bump eth-brownie and safe-eth-py * refactor: remove ethereum network backport * feat: support new safe-eth-py factory we create a Safe first, then use get_version to instantiate the correct brownie safe. it subclasses BrownieSafeBase and a version-specific Safe contract. * fix: network names * refactor: use built in multisend contract detection * fix: web3 snek case * chore: gitignore * fix: api changes * chore: release
1 parent bfbe55e commit 3e54476

File tree

5 files changed

+2059
-1600
lines changed

5 files changed

+2059
-1600
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ cache/
55
build/
66
env/
77
.python-version
8+
scripts/
9+
__pycache__/
10+
.venv/

brownie_safe.py

Lines changed: 84 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1+
from abc import ABCMeta
12
import os
23
import re
3-
import warnings
44
from copy import copy
55
from typing import Dict, List, Optional, Union
6-
from enum import Enum
76
import click
8-
from gnosis.eth import EthereumClient, EthereumNetwork
97
from web3 import Web3 # don't move below brownie import
108
from brownie import Contract, accounts, chain, history, web3
119
from brownie.convert.datatypes import EthAddress
1210
from brownie.network.account import LocalAccount
1311
from brownie.network.transaction import TransactionReceipt
14-
from eth_abi import encode_abi
12+
from eth_abi import encode
1513
from 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
1718
from gnosis.safe.multi_send import MultiSend, MultiSendOperation, MultiSendTx
1819
from gnosis.safe.safe_tx import SafeTx
1920
from gnosis.safe.signatures import signature_split, signature_to_bytes
2021
from gnosis.safe.api import TransactionServiceApi
21-
from gnosis.eth.ethereum_client import EthereumNetworkNotSupported
2222
from hexbytes import HexBytes
2323
from trezorlib import ethereum, tools, ui
2424
from trezorlib.client import TrezorClient
@@ -27,70 +27,6 @@
2727
from 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-
9430
class 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+

docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
=========
33

4+
0.9.0
5+
-----
6+
7+
- add support for latest brownie
8+
- support latest safe-eth-py factory
9+
- remove backports
10+
11+
412
0.8.0
513
-----
614

0 commit comments

Comments
 (0)