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

feat: decode multisend payload and add calls from it #59

Merged
merged 4 commits into from
Jun 12, 2024
Merged
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
49 changes: 38 additions & 11 deletions ape_safe/multisend.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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`
Expand All @@ -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,
}
)
8 changes: 8 additions & 0 deletions tests/functional/test_multisend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading