Skip to content

Instantly share code, notes, and snippets.

@tschubotz
Last active March 5, 2024 05:01
Show Gist options
  • Save tschubotz/64fa85c0cf3b29026206647f18cb332b to your computer and use it in GitHub Desktop.
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
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')
@tschubotz
Copy link
Author

Script written by @Uxio0

Uses the requirements.txt from https://github.com/gnosis/safe-transaction-service/blob/master/requirements.txt

pip install -r requirements.txt
python test_data.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment