Skip to content

Commit 26887cf

Browse files
authored
fix(Multisend): pass-through impersonate kwarg to execTransaction call (#90)
* fix(Multisend): use `sender=self.safe` for calling * refactor(Multisend): use same exception pattern from `as_transaction` * refactor: remove need to use alchemy as default plugin NOTE: We now have `ape-node` with failover from evmchains * test: remove unused `paramtrize` * refactor(Utils): give back `HexBytes` instead of string * refactor(Utils): add `encode_signatures` utility function * refactor(Account): expose impersonation as private API; pass-thru kwarg * refactor(Account): streamline `SafeAccount.create_safe_tx` * refactor(Multisend): fix operation by using new `SafeAccount` API * fix(Account): list `submitter` as `sender=` in execute txn * fix(Test): wrong kwarg for `safe=` (not needed anyways) * fix(Utils): use `HexStr` from `eth-pydantic-types` instead * refactor(Accounts): raise better error on not enough signatures * refactor(Multisend): just broadcast `.as_transaction` for `__call__` * refactor(Test): batch send needed `submitter=` kwarg to work * refactor(CLI): use `submitter=` kwarg to send transaction * refactor(Accounts): move `submitter=` kwarg handling to `create_execute_transaction` * refactor(Client): revert `SafeTxID` type back to `NewType` * refactor(Account): add convert to int for `safeTxGas` in `create_safe_tx` * docs(MultiSend): update docstrings for multisend feature's correctness * refactor(MultiSend): add auto-detection of version using Safe arg
1 parent 827cbc9 commit 26887cf

File tree

8 files changed

+199
-161
lines changed

8 files changed

+199
-161
lines changed

ape-config.yaml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ plugins:
55
- name: foundry
66
- name: solidity
77
- name: etherscan
8-
- name: alchemy
98

109
ethereum:
11-
mainnet:
12-
default_provider: alchemy
1310
local:
1411
default_provider: foundry
1512

@@ -73,13 +70,3 @@ dependencies:
7370
version: 0.7.6
7471
import_remapping:
7572
- "@openzeppelin/contracts=openzeppelin/v3.4.0"
76-
77-
foundry:
78-
fork:
79-
ethereum:
80-
mainnet:
81-
upstream_provider: alchemy
82-
block_number: 15776634
83-
sepolia:
84-
upstream_provider: alchemy
85-
block_number: 3091950

ape_safe/_cli/pending.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ def propose(cli_ctx, ecosystem, safe, data, gas_price, value, receiver, nonce, s
152152

153153
sender = submitter if isinstance(submitter, AccountAPI) else safe.select_signer(for_="sender")
154154
safe.client.post_transaction(
155-
safe_tx, signatures, sender=sender.address, contractTransactionHash=safe_tx_hash
155+
safe_tx,
156+
signatures,
157+
submitter=sender.address,
158+
contractTransactionHash=safe_tx_hash,
156159
)
157160

158161
# Wait for new transaction to appear
@@ -277,6 +280,7 @@ def _execute(safe: "SafeAccount", txn: "UnexecutedTxData", submitter: "AccountAP
277280
signatures: dict[AddressType, MessageSignature] = {
278281
c.owner: MessageSignature.from_rsv(c.signature) for c in txn.confirmations
279282
}
283+
tx_kwargs["submitter"] = submitter
280284
exc_tx = safe.create_execute_transaction(safe_tx, signatures, **tx_kwargs)
281285
submitter.call(exc_tx)
282286

ape_safe/accounts.py

Lines changed: 108 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,19 @@
1111
from ape.contracts import ContractCall
1212
from ape.exceptions import ContractNotFoundError, ProviderNotConnectedError
1313
from ape.logging import logger
14-
from ape.types import AddressType, HexBytes, MessageSignature
14+
from ape.types import AddressType, MessageSignature
1515
from ape.utils import ZERO_ADDRESS, cached_property
1616
from ape_ethereum.proxies import ProxyInfo, ProxyType
1717
from ape_ethereum.transactions import TransactionType
1818
from eip712.common import SafeTxV1, SafeTxV2, create_safe_tx_def
19-
from eth_utils import keccak, to_bytes, to_int
19+
from eth_utils import keccak, to_bytes, to_canonical_address, to_int
2020
from ethpm_types import ContractType
2121
from ethpm_types.abi import ABIType, MethodABI
2222
from packaging.version import Version
2323

24-
from .client import (
25-
BaseSafeClient,
26-
MockSafeClient,
27-
SafeClient,
28-
SafeTx,
29-
SafeTxConfirmation,
30-
SafeTxID,
31-
)
24+
from ape_safe.client.types import OperationType
25+
26+
from .client import BaseSafeClient, MockSafeClient, SafeClient, SafeTx, SafeTxConfirmation, SafeTxID
3227
from .config import SafeConfig
3328
from .exceptions import (
3429
ApeSafeError,
@@ -43,7 +38,7 @@
4338
from .modules import SafeModuleManager
4439
from .packages import PackageType
4540
from .types import SafeCacheData
46-
from .utils import get_safe_tx_hash, order_by_signer
41+
from .utils import encode_signatures, get_safe_tx_hash
4742

4843
if TYPE_CHECKING:
4944
from ape.api.address import BaseAddress
@@ -190,8 +185,7 @@ def get_signatures(
190185
) -> dict[AddressType, MessageSignature]:
191186
signatures: dict[AddressType, MessageSignature] = {}
192187
for signer in signers:
193-
signature = signer.sign_message(safe_tx)
194-
if signature:
188+
if signature := signer.sign_message(safe_tx):
195189
signatures[signer.address] = signature
196190

197191
return signatures
@@ -417,27 +411,39 @@ def create_safe_tx(self, txn: Optional[TransactionAPI] = None, **safe_tx_kwargs)
417411
418412
Args:
419413
txn (Optional[``TransactionAPI``]): The transaction
420-
**safe_tx_kwargs: The safe transactions specifications, such as ``submitter``.
414+
**safe_tx_kwargs: The safe transaction specifications, such as ``operation``.
421415
422416
Returns:
423417
:class:`~ape_safe.client.SafeTx`: The Safe Transaction to be used.
424418
"""
425-
safe_tx = {
426-
"to": txn.receiver if txn else self.address, # Self-call, e.g. rejection
427-
"value": txn.value if txn else 0,
428-
"data": (txn.data or b"") if txn else b"",
429-
"nonce": self.new_nonce if txn is None or txn.nonce is None else txn.nonce,
430-
"operation": 0,
431-
"safeTxGas": 0,
432-
"gasPrice": 0,
433-
"gasToken": ZERO_ADDRESS,
434-
"refundReceiver": ZERO_ADDRESS,
435-
}
436-
safe_tx = {
437-
**safe_tx,
438-
**{k: v for k, v in safe_tx_kwargs.items() if k in safe_tx and v is not None},
439-
}
440-
return self.safe_tx_def(**safe_tx)
419+
return self.safe_tx_def( # type: ignore[call-arg]
420+
# NOTE: Use `txn` only for these 3 commonly-used params
421+
to=(
422+
txn.receiver
423+
if txn
424+
else self.conversion_manager.convert(
425+
safe_tx_kwargs.get("to", self.address), AddressType
426+
)
427+
),
428+
value=(
429+
txn.value
430+
if txn
431+
else self.conversion_manager.convert(safe_tx_kwargs.get("value", 0), int)
432+
),
433+
data=(txn.data or b"") if txn else safe_tx_kwargs.get("data", b""),
434+
# ...then use only kwargs for the rest
435+
# NOTE: Use `.new_nonce` to get latest nonce in off-chain txn queue
436+
nonce=safe_tx_kwargs.get("nonce", self.new_nonce),
437+
operation=safe_tx_kwargs.get("operation", OperationType.CALL),
438+
safeTxGas=self.conversion_manager.convert(safe_tx_kwargs.get("safeTxGas", 0), int),
439+
gasPrice=self.conversion_manager.convert(safe_tx_kwargs.get("gasPrice", 0), int),
440+
gasToken=self.conversion_manager.convert(
441+
safe_tx_kwargs.get("gasToken", ZERO_ADDRESS), AddressType
442+
),
443+
refundReceiver=self.conversion_manager.convert(
444+
safe_tx_kwargs.get("refundReceiver", ZERO_ADDRESS), AddressType
445+
),
446+
)
441447

442448
def create_batch(self) -> "MultiSend":
443449
from ape_safe.multisend import MultiSend
@@ -530,24 +536,69 @@ def local_signers(self) -> list[AccountAPI]:
530536

531537
return list(filter(lambda a: a in signers, accounts))
532538

539+
def _preapproved_signature(
540+
self, signer: Union[AddressType, "BaseAddress", str]
541+
) -> MessageSignature:
542+
# Get the Safe-style "preapproval" signature type, which is a sentinel value used to denote
543+
# when a signer approved via some other method, such as `approveHash` or being `msg.sender`
544+
# TODO: Link documentation for this
545+
return MessageSignature(
546+
v=1, # Approved hash (e.g. submitter is approved)
547+
r=(
548+
b"\x00" * 12
549+
+ to_canonical_address(self.conversion_manager.convert(signer, AddressType))
550+
),
551+
s=b"\x00" * 32,
552+
)
553+
554+
def _impersonate_approvals(self, safe_tx: SafeTx) -> dict[AddressType, MessageSignature]:
555+
safe_tx_hash = self.contract.getTransactionHash(*safe_tx)
556+
557+
# Bypass signature collection logic and attempt to submit by impersonation
558+
# NOTE: Only works for fork and local network providers that support `set_storage`
559+
signatures = dict()
560+
for signer_address in self.signers[: self.confirmations_required]:
561+
# NOTE: `approvedHashes` is `address => safe_tx_hash => num_confs` @ slot 8
562+
# TODO: Use native ape slot indexing, once available
563+
# e.g. `self.contract.approvedHashes[signer_address][safe_tx_hash] = 1`
564+
address_bytes32 = to_bytes(hexstr=signer_address)
565+
address_bytes32 = b"\x00" * (32 - len(address_bytes32)) + address_bytes32
566+
key_hash = keccak(address_bytes32 + b"\x00" * 31 + to_bytes(8))
567+
slot = to_int(keccak(safe_tx_hash + key_hash))
568+
self.provider.set_storage(self.address, slot, b"\x00" * 31 + b"\x01")
569+
570+
signatures[signer_address] = self._preapproved_signature(signer_address)
571+
572+
return signatures
573+
533574
@handle_safe_logic_error()
534575
def create_execute_transaction(
535576
self,
536577
safe_tx: SafeTx,
537578
signatures: Mapping[AddressType, MessageSignature],
579+
submitter: Union[AccountAPI, AddressType, str, None] = None,
580+
impersonate: bool = False,
538581
**txn_options,
539582
) -> TransactionAPI:
540-
exec_args = list(safe_tx._body_["message"].values())[:-1] # NOTE: Skip `nonce`
541-
encoded_signatures = HexBytes(
542-
b"".join(
543-
sig.encode_rsv() if isinstance(sig, MessageSignature) else sig
544-
for sig in order_by_signer(signatures)
545-
)
546-
)
583+
if impersonate:
584+
signatures = self._impersonate_approvals(safe_tx)
585+
586+
elif len(signatures) < self.confirmations_required:
587+
raise NotEnoughSignatures(self.confirmations_required, len(signatures))
588+
589+
if not isinstance(submitter, AccountAPI):
590+
submitter = self.load_submitter(submitter)
591+
assert isinstance(submitter, AccountAPI) # NOTE: mypy happy
592+
593+
exec_args = _safe_tx_exec_args(safe_tx)[:-1] # NOTE: Skip `nonce`
594+
encoded_signatures = encode_signatures(signatures)
547595

548596
# NOTE: executes a `ProviderAPI.prepare_transaction`, which may produce `ContractLogicError`
549597
return self.contract.execTransaction.as_transaction(
550-
*exec_args, encoded_signatures, **txn_options
598+
*exec_args,
599+
encoded_signatures,
600+
**txn_options,
601+
sender=submitter,
551602
)
552603

553604
def compute_prev_signer(self, signer: Union[str, AddressType, "BaseAddress"]) -> AddressType:
@@ -606,56 +657,6 @@ def estimate_gas_cost(self, **kwargs) -> int:
606657
or 0
607658
)
608659

609-
def _preapproved_signature(
610-
self, signer: Union[AddressType, "BaseAddress", str]
611-
) -> MessageSignature:
612-
# Get the Safe-style "preapproval" signature type, which is a sentinel value used to denote
613-
# when a signer approved via some other method, such as `approveHash` or being `msg.sender`
614-
# TODO: Link documentation for this
615-
return MessageSignature(
616-
v=1, # Approved hash (e.g. submitter is approved)
617-
r=b"\x00" * 12 + to_bytes(hexstr=self.conversion_manager.convert(signer, AddressType)),
618-
s=b"\x00" * 32,
619-
)
620-
621-
@handle_safe_logic_error()
622-
def _impersonated_call(self, txn: TransactionAPI, **safe_tx_and_call_kwargs) -> ReceiptAPI:
623-
safe_tx = self.create_safe_tx(txn, **safe_tx_and_call_kwargs)
624-
safe_tx_exec_args = _safe_tx_exec_args(safe_tx)
625-
signatures = {}
626-
627-
# Bypass signature collection logic and attempt to submit by impersonation
628-
# NOTE: Only works for fork and local network providers that support `set_storage`
629-
safe_tx_hash = self.contract.getTransactionHash(*safe_tx_exec_args)
630-
signer_address = None
631-
for signer_address in self.signers[: self.confirmations_required]:
632-
# NOTE: `approvedHashes` is `address => safe_tx_hash => num_confs` @ slot 8
633-
# TODO: Use native ape slot indexing, once available
634-
address_bytes32 = to_bytes(hexstr=signer_address)
635-
address_bytes32 = b"\x00" * (32 - len(address_bytes32)) + address_bytes32
636-
key_hash = keccak(address_bytes32 + b"\x00" * 31 + to_bytes(8))
637-
slot = to_int(keccak(safe_tx_hash + key_hash))
638-
self.provider.set_storage(self.address, slot, to_bytes(1))
639-
640-
signatures[signer_address] = self._preapproved_signature(signer_address)
641-
642-
# NOTE: Could raise a `SafeContractError`
643-
safe_tx_and_call_kwargs["sender"] = safe_tx_and_call_kwargs.get(
644-
"submitter",
645-
# NOTE: Use whatever the last signer was if no `submitter`
646-
self.account_manager.test_accounts[signer_address],
647-
)
648-
return self.contract.execTransaction(
649-
*safe_tx_exec_args[:-1], # NOTE: Skip nonce
650-
HexBytes(
651-
b"".join(
652-
sig.encode_rsv() if isinstance(sig, MessageSignature) else sig
653-
for sig in order_by_signer(signatures)
654-
)
655-
),
656-
**safe_tx_and_call_kwargs,
657-
)
658-
659660
@handle_safe_logic_error()
660661
def call( # type: ignore[override]
661662
self,
@@ -665,13 +666,11 @@ def call( # type: ignore[override]
665666
) -> ReceiptAPI:
666667
# NOTE: This handles if given `submit=None'.
667668
default_submit = not impersonate
668-
submit = (
669+
call_kwargs["submit"] = (
669670
call_kwargs.pop("submit_transaction", call_kwargs.pop("submit", default_submit))
670671
or not default_submit
671672
)
672-
call_kwargs["submit"] = submit
673-
if impersonate:
674-
return self._impersonated_call(txn, **call_kwargs)
673+
call_kwargs["impersonate"] = impersonate
675674

676675
return super().call(txn, **call_kwargs)
677676

@@ -699,8 +698,7 @@ def get_api_confirmations(
699698
}
700699

701700
def _contract_approvals(self, safe_tx: SafeTx) -> Mapping[AddressType, MessageSignature]:
702-
safe_tx_exec_args = _safe_tx_exec_args(safe_tx)
703-
safe_tx_hash = self.contract.getTransactionHash(*safe_tx_exec_args)
701+
safe_tx_hash = self.contract.getTransactionHash(*safe_tx)
704702

705703
return {
706704
signer: self._preapproved_signature(signer)
@@ -715,10 +713,11 @@ def _all_approvals(self, safe_tx: SafeTx) -> dict[AddressType, MessageSignature]
715713
approvals.update(self._contract_approvals(safe_tx))
716714
return approvals
717715

716+
@handle_safe_logic_error()
718717
def submit_safe_tx(
719718
self,
720719
safe_tx: Union[SafeTx, SafeTxID],
721-
submitter: Union[AccountAPI, AddressType, str, None] = None,
720+
impersonate: bool = False,
722721
**txn_options,
723722
) -> ReceiptAPI:
724723
"""
@@ -740,16 +739,22 @@ def submit_safe_tx(
740739

741740
else:
742741
safe_tx_id = get_safe_tx_hash(safe_tx)
743-
744742
assert isinstance(safe_tx, (SafeTxV1, SafeTxV2))
745-
signatures = self._all_approvals(safe_tx)
746-
txn = self.create_execute_transaction(safe_tx, signatures, **txn_options)
747743

748-
if not isinstance(submitter, AccountAPI):
749-
submitter = self.load_submitter(submitter)
750-
assert isinstance(submitter, AccountAPI) # NOTE: mypy happy
744+
if impersonate:
745+
signatures = {}
751746

752-
return submitter.call(txn)
747+
elif len(signatures := self._all_approvals(safe_tx)) < self.confirmations_required:
748+
signatures = self.add_signatures(safe_tx)
749+
750+
txn = self.create_execute_transaction(
751+
safe_tx,
752+
signatures,
753+
impersonate=impersonate,
754+
**txn_options,
755+
)
756+
757+
return self.provider.send_transaction(txn)
753758

754759
def sign_transaction(
755760
self,
@@ -856,13 +861,8 @@ def skip_signer(signer: AccountAPI):
856861
safe_tx,
857862
sigs_by_signer,
858863
**gas_args,
859-
nonce=submitter_account.nonce,
860-
# NOTE: Because of `ape_ethereum.transactions.BaseTransaction.serialize_transaction`
861-
# doing a recovered signer check, and we have to make sure the address matches
862-
# the recovered address of the signed transaction.
863-
sender=submitter_account.address,
864+
submitter=submitter_account,
864865
)
865-
# NOTE: If `submitter` does not sign, this returns `None` which raises downstream
866866
return submitter_account.sign_transaction(exec_transaction, **signer_options)
867867

868868
elif submit:

ape_safe/client/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from ape.types import AddressType, HexBytes
66
from eip712.common import SafeTxV1, SafeTxV2, create_safe_tx_def
7-
from eth_typing import HexStr
7+
from eth_pydantic_types import HexStr
88
from eth_utils import add_0x_prefix, to_hex
99
from pydantic import BaseModel, BeforeValidator, Field, field_validator
1010

0 commit comments

Comments
 (0)