From b4e7943a1694bc0ff4e88e571afb3681b0eff35d Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 13 Jun 2024 03:25:45 +0400 Subject: [PATCH] feat: multisend contract detection (#58) * feat: detect multisend contract instead of hardcoding * feat: disallow delegatecall in multisend calls * lint: happy linter * refactor: use manifest for multisend abi and code * refactor: rename data to manifests * lint: mypy * lint: shut up robot * test: remove test multisend call only doesn't enforce being delegatecalled * lint: happy bot --- ape_safe/manifests/multisend.json | 1 + ape_safe/multisend.py | 108 +++++------------------------ setup.py | 2 +- tests/functional/test_multisend.py | 13 ---- 4 files changed, 21 insertions(+), 103 deletions(-) create mode 100644 ape_safe/manifests/multisend.json diff --git a/ape_safe/manifests/multisend.json b/ape_safe/manifests/multisend.json new file mode 100644 index 0000000..e7df45e --- /dev/null +++ b/ape_safe/manifests/multisend.json @@ -0,0 +1 @@ +{"contractTypes":{"MultiSendCallOnly":{"abi":[{"inputs":[{"internalType":"bytes","name":"transactions","type":"bytes"}],"name":"multiSend","outputs":[],"stateMutability":"payable","type":"function"}],"contractName":"MultiSendCallOnly","methodIdentifiers":{"multiSend(bytes)":"0x8d80ff0a"},"runtimeBytecode":{"bytecode":"0x60806040526004361061001e5760003560e01c80638d80ff0a14610023575b600080fd5b6100dc6004803603602081101561003957600080fd5b810190808035906020019064010000000081111561005657600080fd5b82018360208201111561006857600080fd5b8035906020019184600183028401116401000000008311171561008a57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506100de565b005b805160205b8181101561015f578083015160f81c6001820184015160601c60158301850151603584018601516055850187016000856000811461012857600181146101385761013d565b6000808585888a5af1915061013d565b600080fd5b50600081141561014c57600080fd5b82605501870196505050505050506100e3565b50505056fea264697066735822122035246402746c96964495cae5b36461fd44dfb89f8e6cf6f6b8d60c0aa89f414864736f6c63430007060033"}}},"manifest":"ethpm/3","name":"multisend","version":"v1.3.0"} \ No newline at end of file diff --git a/ape_safe/multisend.py b/ape_safe/multisend.py index 43572b9..b0bc64d 100644 --- a/ape_safe/multisend.py +++ b/ape_safe/multisend.py @@ -1,86 +1,24 @@ +from importlib.resources import files 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 AddressType, ContractType, HexBytes +from ape.types import AddressType, HexBytes from ape.utils import ManagerAccessMixin, cached_property from eth_abi.packed import encode_packed +from ethpm_types import PackageManifest from ape_safe.exceptions import UnsupportedChainError, ValueRequired - -# NOTE: Function name is constant-like because it is used to assemble a constant -# TODO: Do this better -def MULTISEND_CODE(address) -> HexBytes: - return HexBytes( - "0x60806040526004361061001e5760003560e01c80638d80ff0a14610023575b600080fd5b6100dc600480360" - "3602081101561003957600080fd5b810190808035906020019064010000000081111561005657600080fd5b82" - "018360208201111561006857600080fd5b8035906020019184600183028401116401000000008311171561008" - "a57600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383" - "80828437600081840152601f19601f8201169050808301925050505050505091929192905050506100de565b0" - f"05b7f000000000000000000000000{address[2:].lower()}73ffffffffffffffffffffffffffffffffffff" - "ffff163073ffffffffffffffffffffffffffffffffffffffff161415610183576040517f08c379a0000000000" - "00000000000000000000000000000000000000000000000815260040180806020018281038252603081526020" - "01806102106030913960400191505060405180910390fd5b805160205b8181101561020a578083015160f81c6" - "001820184015160601c6015830185015160358401860151605585018701600085600081146101cd5760018114" - "6101dd576101e8565b6000808585888a5af191506101e8565b6000808585895af491505b5060008114156101f" - "757600080fd5b8260550187019650505050505050610188565b50505056fe4d756c746953656e642073686f75" - "6c64206f6e6c792062652063616c6c6564207669612064656c656761746563616c6ca26469706673582212205" - "c784303626eec02b71940b551976170b500a8a36cc5adcbeb2c19751a76d05464736f6c63430007060033" - ) - - -DEFAULT_ADDRESS = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761" -DEPLOYMENT_ADDRESS = { - 10: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 25: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 28: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 61: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 63: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 69: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 82: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 83: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 106: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 111: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 288: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 322: "0x6367360366E4c898488091ac315834B779d8f561", - 338: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 420: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 588: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 595: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 599: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 686: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 787: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 1001: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 1088: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 1294: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 7700: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 8217: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 10000: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 10001: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 42220: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 43114: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 54211: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 71401: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 71402: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 11155111: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 1666600000: "0x998739BFdAAdde7C933B942a68053933098f9EDa", - 1666700000: "0x998739BFdAAdde7C933B942a68053933098f9EDa", -} -MULTISEND_CONTRACT_TYPE = { - "contractName": "MultiSend", - "abi": [ - {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, - { - "inputs": [{"internalType": "bytes", "name": "transactions", "type": "bytes"}], - "name": "multiSend", - "outputs": [], - "stateMutability": "payable", - "type": "function", - }, - ], -} +MULTISEND_CALL_ONLY_ADDRESSES = ( + "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", # MultiSend Call Only v1.3.0 + "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", # MultiSend Call Only v1.3.0 (EIP-155) +) +MULTISEND_CALL_ONLY_MANIFEST = PackageManifest.model_validate_json( + files("ape_safe").joinpath("manifests/multisend.json").read_text() +) +MULTISEND_CALL_ONLY = MULTISEND_CALL_ONLY_MANIFEST.contract_types["MultiSendCallOnly"] # type: ignore # noqa: E501 class MultiSend(ManagerAccessMixin): @@ -138,24 +76,18 @@ def multisend(): assert active_provider, "Must be connected to an active network to deploy" active_provider.set_code( - DEFAULT_ADDRESS, - MULTISEND_CODE(DEFAULT_ADDRESS), + MULTISEND_CALL_ONLY_ADDRESSES[0], MULTISEND_CALL_ONLY.get_runtime_bytecode() ) @cached_property def contract(self) -> ContractInstance: - multisend_address = DEPLOYMENT_ADDRESS.get(self.provider.chain_id, DEFAULT_ADDRESS) - - # All versions have this ABI - contract = self.chain_manager.contracts.instance_at( - multisend_address, - contract_type=ContractType.model_validate(MULTISEND_CONTRACT_TYPE), - ) - - if contract.code != MULTISEND_CODE(multisend_address): - raise UnsupportedChainError() + for address in MULTISEND_CALL_ONLY_ADDRESSES: + if self.provider.get_code(address) == MULTISEND_CALL_ONLY.get_runtime_bytecode(): + return self.chain_manager.contracts.instance_at( + address, contract_type=MULTISEND_CALL_ONLY + ) - return contract + raise UnsupportedChainError() @property def handler(self) -> ContractTransactionHandler: @@ -165,7 +97,6 @@ def add( self, call, *args, - delegatecall=False, value=0, ) -> "MultiSend": """ @@ -177,12 +108,11 @@ def add( Args: call: :class:`ContractMethodHandler` The method to call. *args: The arguments to invoke the method with. - delegatecall: bool Whether the call should be processed using delegatecall. value: int The amount of ether to forward with the call. """ self.calls.append( { - "operation": int(delegatecall), + "operation": 0, "target": call.contract.address, "value": value or 0, "callData": call.encode_input(*args), diff --git a/setup.py b/setup.py index 9ac0fee..cbb4065 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ zip_safe=False, keywords="ethereum", packages=find_packages(exclude=["tests", "tests.*"]), - package_data={"ape_safe": ["py.typed"]}, + package_data={"ape_safe": ["py.typed", "manifests/*"]}, classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", diff --git a/tests/functional/test_multisend.py b/tests/functional/test_multisend.py index 30fd84e..fb666e7 100644 --- a/tests/functional/test_multisend.py +++ b/tests/functional/test_multisend.py @@ -1,8 +1,3 @@ -import pytest - -from ape_safe.exceptions import SafeLogicError - - def test_asset(vault, token): assert vault.asset() == token @@ -15,14 +10,6 @@ def test_default_operation(safe, token, vault, multisend): assert receipt.txn_hash -def test_no_operation(safe, token, vault, multisend): - amount = token.balanceOf(safe) - multisend.add(token.approve, vault, 123) - 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