Ape Safe: Gnosis Safe tx builder¶
Ape Safe allows you to iteratively build complex multi-step Gnosis Safe transactions and safely preview their side effects from the convenience of a locally forked mainnet environment.
It is powered by Brownie and builds upon GnosisPy, extending it with additional capabilities. This tool has been informally known as Chief Multisig Officer at Yearn and has been used to prepare complex transactions with great success.
Since multisig signers are usually slow to fulfill their duties, it’s common to batch multiple actions and send them as one transaction. 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.
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.
pip install -U ape-safe brownie console --network mainnet-fork
from ape_safe import ApeSafe safe = ApeSafe('ychad.eth') dai = safe.contract('0x6B175474E89094C44Da98b954EedeAC495271d0F') vault = safe.contract('0x19D3364A399d251E894aC732651be8B0E4e85001') amount = dai.balanceOf(safe.account) dai.approve(vault, amount) vault.deposit(amount) safe_tx = safe.multisend_from_receipts() safe.preview(safe_tx) safe.post_transaction(safe_tx)
Then you can simply:
pip install -U ape-safe
Several options for signing transactions are available in Ape Safe, including support for hardware wallets.
This is the default signing method when you send a transaction.
Import a private key or a keystore into Brownie to use it with Ape Safe. Brownie accounts are encrypted at rest as .json keystores. See also Brownie’s Account management documentation.
# Import a private key $ brownie accounts new ape Enter the private key you wish to add: # Import a .json keystore $ brownie accounts import ape keystore.json
Ape Safe will prompt you for an account (unless supplied as an argument) and Brownie will prompt you for a password.
>>> safe.sign_transaction(safe_tx) signer (ape, safe): ape Enter password for "ape": >>> safe.sign_transaction(safe_tx, 'ape') Enter password for "ape":
If you prefer to manage accounts outside Brownie, e.g. use a seed phrase, you can pass a
>>> from eth_account import Account >>> key = Account.from_mnemonic('safe grape tape escape...') >>> safe.sign_transaction(safe_tx, key)
If you wish to use a hardware wallet, your best option is Frame. It supports Ledger, Trezor, and Lattice. You can also use with with keystore accounts, they are called Ring Signers in Frame.
To sign, select an account in Frame and do this:
Frame exposes an RPC connection at
http://127.0.0.1:1248 and exposes the currently selected account as
eth_accounts. Ape Safe sends the payload as
eth_signTypedData_v4, which must be supported by your signer device.
Alternative method for Trezor models and firmware versions which don’t support EIP-712 using
Let’s try making a transaction that puts to work the idle DAI in our treasury. We have our eyes on one of the Yearn’s vaults where you need to supply a Curve LP token.
The vault token is a few hops away from DAI:
Deposit DAI into Curve Pool, receive Curve LP token.
Deposit Curve LP into Yearn Vault, receive Vault shares.
Now drop into Brownie’s interactive console:
$ brownie console --network mainnet-fork
Play around the same way you would do with a normal account:
>>> 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'> # Instantiate contracts with `safe.contract` >>> dai = safe.contract('0x6B175474E89094C44Da98b954EedeAC495271d0F') >>> zap = safe.contract('0x094d12e5b541784701FD8d65F11fc0598FBC6332') >>> lp = safe.contract('0x4f3E8F405CF5aFC05D68142F3783bDfE13811522') >>> vault = safe.contract('0xFe39Ce91437C76178665D64d7a2694B0f6f17fE3') # Work our way towards having a vault balance >>> dai_amount = dai.balanceOf(safe) >>> dai.approve(zap, dai_amount) >>> amounts = [0, dai_amount, 0, 0] >>> mint_amount = zap.calc_token_amount(amounts, True) >>> zap.add_liquidity(amounts, mint_amount * 0.99) >>> lp.approve(vault, 2 ** 256 - 1) >>> vault.depositAll() >>> vault.balanceOf(safe) 2609.5479641693646 # Combine transaction history into a multisend transaction >>> safe_tx = safe.multisend_from_receipts() # Estimate the gas needed for a successful execution >>> safe.estimate_gas(safe_tx) 1082109 # Preview the side effects in mainnet fork, # including a detailed call trace, courtesy of Brownie >>> safe.preview(safe_tx, call_trace=True) # Post it to the transaction service # Prompts for a signature if needed >>> safe.post_transaction(safe_tx) # Post an additional confirmation to the transaction service >>> signtature = safe.sign_transaction(safe_tx) >>> safe.post_signature(safe_tx, signature) # Retrieve pending transactions from the transaction service >>> safe.pending_transactions # Preview the side effects of all pending transactions >>> safe.preview_pending() # Execute the transactions with enough signatures >>> network.priority_fee('2 gwei') >>> signer = safe.get_signer('ape') >>> >>> for tx in safe.pending_transactions: >>> receipt = safe.execute_transaction(safe_tx, signer) >>> receipt.info()
hardware wallet support via frame
submit signatures to transaction service
retrieve pending transactions from transaction service
execute signed transactions
convert confirmations to signatures
expanded documentation about signing
add support for safe contracts 1.3.0
switch to multicall 1.3.0 call only
support multiple networks
autodetect transaction service from chain id
- class ape_safe.ApeSafe(address, base_url=None, multisend=None)¶
- property account: brownie.network.account.LocalAccount¶
Unlocked Brownie account for Gnosis Safe.
- confirmations_to_signatures(confirmations: List[Dict]) bytes ¶
Convert confirmations as returned by the transaction service to combined signatures.
- contract(address) brownie.network.contract.Contract ¶
Instantiate a Brownie Contract owned by Safe account.
- estimate_gas(safe_tx: gnosis.safe.safe_tx.SafeTx) int ¶
Estimate gas limit for successful execution.
- execute_transaction(safe_tx: gnosis.safe.safe_tx.SafeTx, signer=None) brownie.network.transaction.TransactionReceipt ¶
Execute a fully signed transaction likely retrieved from the pending_transactions method.
- get_signer(signer: Optional[Union[brownie.network.account.LocalAccount, str]] = None) brownie.network.account.LocalAccount ¶
- multisend_from_receipts(receipts: Optional[List[brownie.network.transaction.TransactionReceipt]] = None, safe_nonce: Optional[int] = None) gnosis.safe.safe_tx.SafeTx ¶
Convert multiple Brownie transaction receipts (or history) to a multisend Safe transaction.
- pending_nonce() int ¶
Subsequent nonce which accounts for pending transactions in the transaction service.
- property pending_transactions: List[gnosis.safe.safe_tx.SafeTx]¶
Retrieve pending transactions from the transaction service.
- post_signature(safe_tx: gnosis.safe.safe_tx.SafeTx, signature: bytes)¶
Submit a confirmation signature to a transaction service.
- post_transaction(safe_tx: gnosis.safe.safe_tx.SafeTx)¶
Submit a Safe transaction to a transaction service. Prompts for a signature if needed.
- preview(safe_tx: gnosis.safe.safe_tx.SafeTx, events=True, call_trace=False, reset=True)¶
Dry run a Safe transaction in a forked network environment.
- preview_pending(events=True, call_trace=False)¶
Dry run all pending transactions in a forked environment.
- sign_transaction(safe_tx: gnosis.safe.safe_tx.SafeTx, signer=None) gnosis.safe.safe_tx.SafeTx ¶
Sign a Safe transaction with a private key account.
- sign_with_frame(safe_tx: gnosis.safe.safe_tx.SafeTx, frame_rpc='http://127.0.0.1:1248') bytes ¶
Sign a Safe transaction using Frame. Use this option with hardware wallets.
- sign_with_trezor(safe_tx: gnosis.safe.safe_tx.SafeTx, derivation_path="m/44'/60'/0'/0/0", use_passphrase=False)¶
Sign a Safe transaction with a Trezor wallet.
Creates an eth_sign signature since Trezor EIP-712 isn’t available yet as of TT fw v2.4.2, T1 fw v1.10.3
Defaults to no passphrase (and skips passphrase prompt) by default. Uses on-device passphrase input if use_passphrase is truthy
- tx_from_receipt(receipt: brownie.network.transaction.TransactionReceipt, operation: gnosis.safe.safe.SafeOperation = SafeOperation.CALL, safe_nonce: Optional[int] = None) gnosis.safe.safe_tx.SafeTx ¶
Convert Brownie transaction receipt to a Safe transaction.
- exception ape_safe.ApiError¶
- exception ape_safe.ExecutionFailure¶