From 00f714260e776f8e53047eb485f1faf6265ea2e8 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 21 Sep 2021 21:16:09 +0200 Subject: [PATCH 1/3] Fix gitignore --- .gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.gitignore b/.gitignore index ede3a45..ef70c07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,17 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + dist/ _build/ poetry.lock .hypothesis/ +.idea/ + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json From 0d1cfedd3bd4ec5e67747f3390928e8b4c42b101 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 21 Sep 2021 21:18:18 +0200 Subject: [PATCH 2/3] Use brownie preview() function to estimate gas --- ape_safe.py | 35 ++++++++++++++++++++++------------- docs/conf.py | 1 + docs/detailed.rst | 6 +++--- docs/intro.rst | 2 +- pyproject.toml | 2 +- tests/test_sorting.py | 9 ++++----- 6 files changed, 32 insertions(+), 23 deletions(-) diff --git a/ape_safe.py b/ape_safe.py index 9606f0e..5b7b3a7 100644 --- a/ape_safe.py +++ b/ape_safe.py @@ -15,7 +15,6 @@ from gnosis.safe.multi_send import MultiSend, MultiSendOperation, MultiSendTx from gnosis.safe.safe_tx import SafeTx - MULTISEND_CALL_ONLY = '0x40A2aCCbd92BCA938b02010E17A5b8929b49130D' multisends = { 250: '0x10B62CC1E8D9a9f1Ad05BCC491A7984697c19f7E', @@ -89,7 +88,7 @@ def tx_from_receipt(self, receipt: TransactionReceipt, operation: SafeOperation """ if safe_nonce is None: safe_nonce = self.pending_nonce() - + return self.build_multisig_tx(receipt.receiver, receipt.value, receipt.input, operation=operation.value, safe_nonce=safe_nonce) def multisend_from_receipts(self, receipts: List[TransactionReceipt] = None, safe_nonce: int = None) -> SafeTx: @@ -98,10 +97,10 @@ def multisend_from_receipts(self, receipts: List[TransactionReceipt] = None, saf """ if receipts is None: receipts = history.from_sender(self.address) - + if safe_nonce is None: safe_nonce = self.pending_nonce() - + txs = [MultiSendTx(MultiSendOperation.CALL, tx.receiver, tx.value, tx.input) for tx in receipts] data = MultiSend(self.multisend, self.ethereum_client).build_tx_data(txs) return self.build_multisig_tx(self.multisend, 0, data, SafeOperation.DELEGATE_CALL.value, safe_nonce=safe_nonce) @@ -112,12 +111,12 @@ def sign_transaction(self, safe_tx: SafeTx, signer: Union[LocalAccount, str] = N """ if signer is None: signer = click.prompt('signer', type=click.Choice(accounts.load())) - + if isinstance(signer, str): # Avoids a previously impersonated account with no signing capabilities accounts.clear() signer = accounts.load(signer) - + safe_tx.sign(signer.private_key) return safe_tx @@ -130,10 +129,11 @@ def post_transaction(self, safe_tx: SafeTx): """ if not safe_tx.sorted_signers: self.sign_transaction(safe_tx) - + sender = safe_tx.sorted_signers[0] url = urljoin(self.base_url, f'/api/v1/safes/{self.address}/multisig-transactions/') + data = { 'to': safe_tx.to, 'value': safe_tx.value, @@ -154,13 +154,21 @@ def post_transaction(self, safe_tx: SafeTx): if not response.ok: raise ApiError(f'Error posting transaction: {response.content}') - def estimate_gas(self, safe_tx: SafeTx) -> int: + def estimate_gas(self, safe_tx: SafeTx, update_safe_tx_gas: bool = False, multiplier: float = 1.1) -> int: """ - Estimate gas limit for successful execution. + Estimate gas limit for successful execution. If `update_tx_gas=True` provided SafeTx will have `safe_tx_gas` + and `base_gas` updated """ - return self.estimate_tx_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation) + # return self.estimate_tx_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation) + gas_estimation = 20_000 + int(multiplier * self.preview(safe_tx, events=False, print_details=False).gas_used) + if update_safe_tx_gas: + safe_tx.safe_tx_gas = gas_estimation + safe_tx.base_gas = self.estimate_tx_base_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation, + safe_tx.gas_token, safe_tx.safe_tx_gas) + safe_tx.signatures = b'' # As we are modifying the tx, previous signatures are not valid anymore + return gas_estimation - def preview(self, safe_tx: SafeTx, events=True, call_trace=False, reset=True, gas_limit=None): + def preview(self, safe_tx: SafeTx, events=True, call_trace=False, reset=True, gas_limit=None, print_details=True): """ Dry run a Safe transaction in a forked network environment. """ @@ -199,7 +207,7 @@ def preview(self, safe_tx: SafeTx, events=True, call_trace=False, reset=True, ga receipt.info() receipt.call_trace(True) raise ExecutionFailure() - + if events: receipt.info() @@ -209,7 +217,8 @@ def preview(self, safe_tx: SafeTx, events=True, call_trace=False, reset=True, ga # Offset gas refund for clearing storage when on-chain signatures are consumed. # https://github.com/gnosis/safe-contracts/blob/v1.1.1/contracts/GnosisSafe.sol#L140 refunded_gas = 15_000 * (threshold - 1) - click.secho(f'recommended gas limit: {receipt.gas_used + refunded_gas}', fg='green', bold=True) + if print_details: + click.secho(f'recommended gas limit: {receipt.gas_used + refunded_gas}', fg='green', bold=True) return receipt diff --git a/docs/conf.py b/docs/conf.py index 323add0..3808665 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath('..')) diff --git a/docs/detailed.rst b/docs/detailed.rst index ca4b04d..076168b 100644 --- a/docs/detailed.rst +++ b/docs/detailed.rst @@ -21,11 +21,11 @@ Play around the same way you would do with a normal account: .. code-block:: python >>> from ape_safe import ApeSafe - + # You can specify an ENS name here # Specify an EthereumClient if you don't run a local node >>> safe = ApeSafe('ychad.eth') - + # Unlocked account is available as `safe.account` >>> safe.account @@ -46,7 +46,7 @@ Play around the same way you would do with a normal account: >>> vault.depositAll() >>> vault.balanceOf(safe.account) 2609.5479641693646 - + # Combine transaction history into a multisend transaction >>> safe_tx = safe.multisend_from_receipts() diff --git a/docs/intro.rst b/docs/intro.rst index 5510234..d823e24 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -5,6 +5,6 @@ Since multisig signers are usually slow to fulfill their duties, it's common to Batching also serves as a rudimentary zap if you are not concerned about the exactly matching in/out values and allow for some slippage. Gnosis Safe has an excellent Transaction Builder app that allows scaffolding complex interactions. -This approach is usually faster and cheaper than deploying a bespoke contract for every transaction. +This approach is usually faster and cheaper than deploying a bespoke contract for every transaction. Ape Safe expands on this idea. It allows you to use multisig as a regular account and then convert the transaction history into one multisend transaction and make sure it works before it hits the signers. diff --git a/pyproject.toml b/pyproject.toml index d0a8dab..8d05feb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "readme.md" [tool.poetry.dependencies] python = "^3.8" eth-brownie = "^1.16.3" -gnosis-py = "^3.2.2" +gnosis-py = "^3.3.3" [tool.poetry.dev-dependencies] diff --git a/tests/test_sorting.py b/tests/test_sorting.py index 6d84ca0..88c18e7 100644 --- a/tests/test_sorting.py +++ b/tests/test_sorting.py @@ -1,13 +1,13 @@ import pytest - -from brownie.test import given, strategy from brownie.convert.datatypes import EthAddress +from brownie.test import given, strategy + @pytest.mark.xfail @given(strategy('uint256[]', max_value=0xffffffffffffffffffffffffffffffffffffffff)) def test_sorting_checksum(addrs): addrs = [EthAddress(addr.to_bytes(20, 'big', signed=False)) for addr in addrs] - + sort_old = sorted(addrs) sort_new = sorted(addrs, key=lambda addr: int(addr, 16)) @@ -17,9 +17,8 @@ def test_sorting_checksum(addrs): @given(strategy('uint256[]', max_value=0xffffffffffffffffffffffffffffffffffffffff)) def test_sorting_lower(addrs): addrs = [EthAddress(addr.to_bytes(20, 'big', signed=False)) for addr in addrs] - + sort_old = sorted(addrs, key=lambda addr: addr.lower()) sort_new = sorted(addrs, key=lambda addr: int(addr, 16)) assert sort_old == sort_new - From 563b19432360acfd08f05965d7492658fe4f534a Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 7 Oct 2021 19:07:14 +0200 Subject: [PATCH 3/3] Optimize gas estimation --- ape_safe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ape_safe.py b/ape_safe.py index 5b7b3a7..0fbb63c 100644 --- a/ape_safe.py +++ b/ape_safe.py @@ -157,14 +157,14 @@ def post_transaction(self, safe_tx: SafeTx): def estimate_gas(self, safe_tx: SafeTx, update_safe_tx_gas: bool = False, multiplier: float = 1.1) -> int: """ Estimate gas limit for successful execution. If `update_tx_gas=True` provided SafeTx will have `safe_tx_gas` - and `base_gas` updated + updated """ # return self.estimate_tx_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation) - gas_estimation = 20_000 + int(multiplier * self.preview(safe_tx, events=False, print_details=False).gas_used) + gas_used = self.preview(safe_tx, events=False, print_details=False).gas_used + safe_tx_gas = max(gas_used * 64 // 63, gas_used + 2500) + 500 + gas_estimation = 35_000 + int(multiplier * safe_tx_gas) if update_safe_tx_gas: safe_tx.safe_tx_gas = gas_estimation - safe_tx.base_gas = self.estimate_tx_base_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation, - safe_tx.gas_token, safe_tx.safe_tx_gas) safe_tx.signatures = b'' # As we are modifying the tx, previous signatures are not valid anymore return gas_estimation