Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use preview estimate gas #15

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
35 changes: 22 additions & 13 deletions ape_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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`
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_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.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.
"""
Expand Down Expand Up @@ -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()

Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#
import os
import sys

sys.path.insert(0, os.path.abspath('..'))


Expand Down
6 changes: 3 additions & 3 deletions docs/detailed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Account '0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52'>
Expand All @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
9 changes: 4 additions & 5 deletions tests/test_sorting.py
Original file line number Diff line number Diff line change
@@ -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))

Expand All @@ -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