Last active
March 5, 2024 05:01
-
-
Save tschubotz/64fa85c0cf3b29026206647f18cb332b to your computer and use it in GitHub Desktop.
Script to submit a number of Multisend transactions to a Gnosis Safe via the REST API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import argparse | |
import json | |
from random import randrange | |
from typing import List, Optional, Sequence | |
from urllib.parse import urljoin | |
import requests | |
from eth_account import Account | |
from eth_account.signers.local import LocalAccount | |
from hexbytes import HexBytes | |
from web3 import Web3 | |
from gnosis.eth.constants import NULL_ADDRESS | |
from gnosis.eth.contracts import get_erc20_contract, get_multi_send_contract | |
from gnosis.safe import SafeTx | |
from gnosis.safe.multi_send import MultiSend, MultiSendOperation, MultiSendTx | |
base_url = 'https://safe-transaction.rinkeby.gnosis.io' | |
dummy_w3 = Web3() | |
dummy_options = {'gas': 1, 'gasPrice': 1, 'nonce': 0, | |
'to': Account.create().address, 'chainId': 44} | |
parser = argparse.ArgumentParser(description='Process some integers.') | |
parser.add_argument('safe_address', type=str, help='Address of the Safe') | |
parser.add_argument('owner_private_key', type=str, help='Owner of the Safe to use as sender') | |
test_abi = json.loads('[{"inputs":[{"internalType":"address","name":"paramAddress","type":"address"}],"name":"testAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"paramArrayAddress","type":"address[]"}],"name":"testArrayAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool[]","name":"paramArrayBool","type":"bool[]"}],"name":"testArrayBool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"paramArrayBytes","type":"bytes[]"}],"name":"testArrayBytes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"paramArrayBytes32","type":"bytes[]"}],"name":"testArrayBytes32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"paramArrayInteger","type":"uint256[]"}],"name":"testArrayInteger","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"name":"testArrayNoName","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"paramBool","type":"bool"}],"name":"testBool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"","type":"bool"}],"name":"testBoolNoName","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"paramBytes","type":"bytes"}],"name":"testBytes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"paramBytes16","type":"bytes32"}],"name":"testBytes16","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"paramBytes32","type":"bytes32"}],"name":"testBytes32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"paramInteger","type":"uint256"}],"name":"testInteger","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"paramInteger16","type":"uint8"}],"name":"testInteger16","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"paramInteger16","type":"uint8"}],"name":"testInteger8","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"paramLongName_abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789","type":"uint256"}],"name":"testIntegerLongParameterName","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"testIntegerNoName","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"p1","type":"uint256"},{"internalType":"bool","name":"p2","type":"bool"},{"internalType":"bytes32","name":"p3","type":"bytes32"},{"internalType":"address","name":"p4","type":"address"},{"internalType":"bool[][]","name":"p5","type":"bool[][]"},{"internalType":"bytes32[]","name":"p6","type":"bytes32[]"},{"internalType":"address[]","name":"p7","type":"address[]"}],"name":"testManyParams","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"p1","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"p4","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bool[][]","name":"","type":"bool[][]"},{"internalType":"bytes32[]","name":"p7","type":"bytes32[]"},{"internalType":"address[]","name":"p8","type":"address[]"}],"name":"testManyParamsMix","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bool","name":"","type":"bool"},{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bool[][]","name":"","type":"bool[][]"},{"internalType":"bytes32[]","name":"","type":"bytes32[]"},{"internalType":"address[]","name":"","type":"address[]"}],"name":"testManyParamsNoName","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool[][][][]","name":"paramNestedArrayBool","type":"bool[][][][]"}],"name":"testNestedArrayBool4Levels","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[][]","name":"paramNestedArrayInteger","type":"uint256[][]"}],"name":"testNestedArrayInteger","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool[][][][][][][][][][]","name":"paramNestedArrayBool","type":"bool[][][][][][][][][][]"}],"name":"testNestedArrayInteger10Levels","outputs":[],"stateMutability":"nonpayable","type":"function"}]') | |
test_contract = dummy_w3.eth.contract(abi=test_abi) | |
nonce_count = 0 | |
def get_nonce(safe_address: str) -> int: | |
url = urljoin(base_url, f'/api/v1/safes/{safe_address}/') | |
return requests.get(url).json()['nonce'] | |
def build_tx_data(multi_send_txs: Sequence[MultiSendTx]) -> bytes: | |
""" | |
Txs don't need to be valid to get through | |
:param multi_send_txs: | |
:param sender: | |
:return: | |
""" | |
multisend_contract = get_multi_send_contract(dummy_w3) | |
encoded_multisend_data = b''.join([x.encoded_data for x in multi_send_txs]) | |
return multisend_contract.functions.multiSend(encoded_multisend_data).buildTransaction(dummy_options)['data'] | |
def post_transaction(safe_address: str, data: bytes, owner_account: LocalAccount, notes: str = ''): | |
url = f'https://safe-transaction.rinkeby.gnosis.io/api/v1/safes/{safe_address}/multisig-transactions/' | |
global nonce_count | |
nonce = get_nonce(safe_address) + nonce_count | |
nonce_count += 1 | |
to = Account.create().address | |
value = 0 | |
operation = 0 | |
safe_tx_gas = 300000 | |
base_gas = 100000 | |
gas_price = 0 | |
gas_token = NULL_ADDRESS | |
refund_receiver = NULL_ADDRESS | |
safe_tx = SafeTx(None, safe_address, to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, | |
refund_receiver, safe_nonce=nonce) | |
safe_tx.sign(owner_account.key) | |
response = requests.post(url, | |
json={ | |
"to": to, | |
"value": value, | |
"data": HexBytes(data).hex(), | |
"operation": operation, | |
"gasToken": gas_token, | |
"safeTxGas": safe_tx_gas, | |
"baseGas": base_gas, | |
"gasPrice": gas_price, | |
"refundReceiver": refund_receiver, | |
"nonce": nonce, | |
"contractTransactionHash": safe_tx.safe_tx_hash.hex(), | |
"sender": owner_account.address, | |
"signature": HexBytes(safe_tx.signatures).hex(), | |
"origin": f"script tester - {notes}", | |
}) | |
assert response.ok, response.content.decode() | |
def get_transfer_data(to: Optional[str] = None, amount: Optional[int] = None): | |
""" | |
Function that gets decoded (erc20 transfer) | |
:return: | |
""" | |
to = Account.create().address if to is None else to | |
amount = 12345678 if amount is None else amount | |
return get_erc20_contract( | |
dummy_w3 | |
).functions.transfer( | |
to, amount | |
).buildTransaction(dummy_options)['data'] | |
def get_test_nested_array_integer_data() -> bytes: | |
return HexBytes(test_contract.functions.testNestedArrayInteger([ | |
[1, 2, 3, 4], [3, 4], [6, 7, 8, 9], | |
[10, 11], | |
[1, 2, 3, 4], [6, 7, 8, 9], [3, 4] | |
]).buildTransaction(dummy_options)['data']) | |
def get_test_nested_array_integer_10_levels() -> bytes: | |
base_list = [] | |
original_list = base_list | |
for _ in range(8): | |
child_list = [] | |
base_list.append(child_list) | |
base_list = child_list | |
child_list.append(list([bool(i % 2) for i in range(10)])) | |
return HexBytes(test_contract.functions.testNestedArrayInteger10Levels( | |
original_list | |
).buildTransaction(dummy_options)['data']) | |
def get_test_many_params_mix() -> bytes: | |
return HexBytes(test_contract.functions.testManyParamsMix( | |
4815, | |
True, | |
HexBytes('0xabcdefabcdef'), | |
Account.create().address, | |
[4815, 162342, 108], | |
[[True, False, True], [True, True], [], [False]], | |
[HexBytes('0xabcdefabcdef'), HexBytes('0x'), HexBytes('0x12'), HexBytes('0xabcdefabcdefab')], | |
list([Account.create().address for _ in range(7)]) | |
).buildTransaction(dummy_options)['data']) | |
def get_not_decoded_data(): | |
""" | |
Function that does not get decoded | |
:return: | |
""" | |
data = get_transfer_data() | |
return HexBytes(HexBytes('0x1122') + HexBytes(data)[2:]) | |
def get_multisig_nesting(level: int, tx_data: bytes) -> MultiSendTx: | |
""" | |
:param level: >= 1 | |
:param tx_data: Data of the non multisig transaction, like transfer | |
:return: | |
""" | |
parent = MultiSendTx(MultiSendOperation.CALL, erc20_address, 0, tx_data) | |
for _ in range(level): | |
parent = MultiSendTx(MultiSendOperation.CALL, erc20_address, 0, build_tx_data([parent])) | |
return parent | |
if __name__ == '__main__': | |
args = parser.parse_args() | |
safe_address = args.safe_address | |
owner_account = Account.from_key(args.owner_private_key) | |
# Action: Call a function that gets decoded, e.g. ERC20 token transfer | |
erc20_address = Account.create().address | |
multisend_tx_decoded = MultiSendTx(MultiSendOperation.CALL, erc20_address, 0, get_transfer_data(erc20_address)) | |
data_multisend_decoded = build_tx_data([multisend_tx_decoded]) | |
post_transaction(safe_address, data_multisend_decoded, owner_account, '1 - Function decoded') | |
# Action: Call any function that doesn't get decoded. | |
erc20_address = Account.create().address | |
multisend_tx_not_decoded = MultiSendTx(MultiSendOperation.CALL, erc20_address, 0, get_not_decoded_data()) | |
data = build_tx_data([multisend_tx_not_decoded]) | |
post_transaction(safe_address, data, owner_account) | |
# Action 1: Call a function that gets decoded, e.g. ERC20 token transfer | |
# Action 2: Call any function that doesn't get decoded" | |
data = build_tx_data([multisend_tx_decoded, multisend_tx_not_decoded]) | |
post_transaction(safe_address, data, owner_account, '1 - Function decoded. 2 - Function not decoded') | |
# 5 actions - Not decoded | |
data = build_tx_data([multisend_tx_not_decoded for _ in range(5)]) | |
post_transaction(safe_address, data, owner_account, '5 Actions not decoded') | |
# 5 actions - Actions need to all go to different destination contracts to check that different indenticons | |
# and addresses are used on actions list | |
data = build_tx_data([multisend_tx_decoded for _ in range(5)]) | |
post_transaction(safe_address, data, owner_account, '5 Actions to different addresses (test identicons)') | |
# Action 1: Call a function that gets decoded, e.g. ERC20 token transfer | |
# Action 2: Another Multisend with 1 action which can be any function." | |
data = build_tx_data([multisend_tx_decoded, | |
MultiSendTx(MultiSendOperation.CALL, erc20_address, 0, data_multisend_decoded)]) | |
post_transaction(safe_address, data, owner_account, '1 - Function decoded. 2 - Multisend to any function') | |
# 5 actions - 3 are Multisend | |
data = build_tx_data([multisend_tx_decoded for _ in range(2)] | |
+ [MultiSendTx(MultiSendOperation.CALL, erc20_address, 0, data_multisend_decoded) | |
for _ in range(3)]) | |
post_transaction(safe_address, data, owner_account, '5 Actions, 3 are Multisend') | |
# 5 levels of Multisend nesting | |
data = build_tx_data([get_multisig_nesting(5, data_multisend_decoded)]) | |
post_transaction(safe_address, data, owner_account, '5 levels of Multisend nesting') | |
# 10 levels of Multisend nesting | |
data = build_tx_data([get_multisig_nesting(10, data_multisend_decoded)]) | |
post_transaction(safe_address, data, owner_account, '10 levels of Multisend nesting') | |
# MultiSend calling functions of the contract | |
erc20_address = Account.create().address | |
data_multisend_decoded = build_tx_data([ | |
MultiSendTx(MultiSendOperation.CALL, Account.create().address, 0, get_test_nested_array_integer_data()), | |
MultiSendTx(MultiSendOperation.CALL, Account.create().address, 0, get_test_nested_array_integer_10_levels()), | |
MultiSendTx(MultiSendOperation.CALL, Account.create().address, 0, get_test_many_params_mix()), | |
]) | |
post_transaction(safe_address, data_multisend_decoded, owner_account, 'Calling functions of test contract') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Script written by @Uxio0
Uses the
requirements.txt
from https://github.com/gnosis/safe-transaction-service/blob/master/requirements.txtpip install -r requirements.txt
python test_data.py