Skip to content

Commit

Permalink
feat: multisend contract detection (#58)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
banteg authored Jun 12, 2024
1 parent 3cdb092 commit b4e7943
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 103 deletions.
1 change: 1 addition & 0 deletions ape_safe/manifests/multisend.json
Original file line number Diff line number Diff line change
@@ -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"}
108 changes: 19 additions & 89 deletions ape_safe/multisend.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -165,7 +97,6 @@ def add(
self,
call,
*args,
delegatecall=False,
value=0,
) -> "MultiSend":
"""
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 0 additions & 13 deletions tests/functional/test_multisend.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import pytest

from ape_safe.exceptions import SafeLogicError


def test_asset(vault, token):
assert vault.asset() == token

Expand All @@ -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
Expand Down

0 comments on commit b4e7943

Please sign in to comment.