diff --git a/ape_safe/multisend.py b/ape_safe/multisend.py index 2cbcd68..43572b9 100644 --- a/ape_safe/multisend.py +++ b/ape_safe/multisend.py @@ -1,6 +1,9 @@ +from io import BytesIO + +from ape import convert from ape.api import ReceiptAPI, TransactionAPI from ape.contracts.base import ContractInstance, ContractTransactionHandler -from ape.types import ContractType, HexBytes +from ape.types import AddressType, ContractType, HexBytes from ape.utils import ManagerAccessMixin, cached_property from eth_abi.packed import encode_packed @@ -112,24 +115,24 @@ class MultiSend(ManagerAccessMixin): def __init__(self) -> None: """ - Initialize a new Multicall session object. By default, there are no calls to make. + Initialize a new MultiSend session object. By default, there are no calls to make. """ self.calls: list[dict] = [] @classmethod def inject(cls): """ - Create the multicall module contract on-chain, so we can use it. + Create the multisend module contract on-chain, so we can use it. Must use a provider that supports ``debug_setCode``. Usage example:: - from ape_ethereum import multicall + from ape_safe.multisend import MultiSend @pytest.fixture(scope="session") - def use_multicall(): - # NOTE: use this fixture any test where you want to use a multicall - multicall.BaseMulticall.deploy() + def multisend(): + MultiSend.inject() + return MultiSend() """ active_provider = cls.network_manager.active_provider assert active_provider, "Must be connected to an active network to deploy" @@ -166,7 +169,7 @@ def add( value=0, ) -> "MultiSend": """ - Adds a call to the Multicall session object. + Append a call to the MultiSend session object. Raises: :class:`InvalidOption`: If one of the kwarg modifiers is not able to be used. @@ -218,11 +221,11 @@ def encoded_calls(self): def __call__(self, **txn_kwargs) -> ReceiptAPI: """ - Execute the Multicall transaction. The transaction will broadcast again every time + Execute the MultiSend transaction. The transaction will broadcast again every time the ``Transaction`` object is called. Raises: - :class:`UnsupportedChain`: If there is not an instance of Multicall3 deployed + :class:`UnsupportedChain`: If there is not an instance of MultiSend deployed on the current chain at the expected address. Args: @@ -238,7 +241,7 @@ def __call__(self, **txn_kwargs) -> ReceiptAPI: def as_transaction(self, **txn_kwargs) -> TransactionAPI: """ - Encode the Multicall transaction as a ``TransactionAPI`` object, but do not execute it. + Encode the MultiSend transaction as a ``TransactionAPI`` object, but do not execute it. Returns: :class:`~ape.api.transactions.TransactionAPI` @@ -253,3 +256,27 @@ def as_transaction(self, **txn_kwargs) -> TransactionAPI: data=self.handler.encode_input(b"".join(self.encoded_calls)), **txn_kwargs, ) + + def add_from_calldata(self, calldata: bytes): + """ + Decode all calls from a multisend calldata and add them to this MultiSend. + + Args: + calldata: Calldata encoding the MultiSend.multiSend call + """ + _, args = self.contract.decode_input(calldata) + buffer = BytesIO(args["transactions"]) + while buffer.tell() < len(args["transactions"]): + operation = int.from_bytes(buffer.read(1), "big") + target = convert(buffer.read(20), AddressType) + value = int.from_bytes(buffer.read(32), "big") + length = int.from_bytes(buffer.read(32), "big") + data = HexBytes(buffer.read(length)) + self.calls.append( + { + "operation": operation, + "target": target, + "value": value, + "callData": data, + } + ) diff --git a/tests/functional/test_multisend.py b/tests/functional/test_multisend.py index 9d4712b..30fd84e 100644 --- a/tests/functional/test_multisend.py +++ b/tests/functional/test_multisend.py @@ -21,3 +21,11 @@ def test_no_operation(safe, token, vault, multisend): multisend.add(vault.transfer, safe, amount) with pytest.raises(SafeLogicError, match="Safe transaction failed"): multisend(sender=safe, operation=0) + + +def test_decode_multisend(multisend): + calldata = bytes.fromhex( + "8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000016b00527e80008d212e2891c737ba8a2768a7337d7fd200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f0080878000000000000000000000000584bffc5f51ccae39ad69f1c399743620e619c2b00da18f789a1d9ad33e891253660fcf1332d236b2900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024e74b981b000000000000000000000000584bffc5f51ccae39ad69f1c399743620e619c2b0027b5739e22ad9033bcbf192059122d163b60349d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247a55036500000000000000000000000000000000000000000000000000002a1b324b8f68000000000000000000000000000000000000000000" # noqa: E501 + ) + multisend.add_from_calldata(calldata) + assert multisend.handler.encode_input(b"".join(multisend.encoded_calls)) == calldata