Skip to content
Open
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
9 changes: 7 additions & 2 deletions hyperliquid/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ def __init__(
self.info = Info(base_url, True, meta, spot_meta, perp_dexs, timeout)
self.expires_after: Optional[int] = None

def _post_action(self, action, signature, nonce):
def _post_action(self, action, signature, nonce, vault_address=None):
if vault_address is None:
vault_address = self.vault_address
payload = {
"action": action,
"nonce": nonce,
"signature": signature,
"vaultAddress": self.vault_address if action["type"] not in ["usdClassTransfer", "sendAsset"] else None,
"vaultAddress": vault_address if action["type"] not in ["usdClassTransfer", "sendAsset"] else None,
"expiresAfter": self.expires_after,
}
logging.debug(payload)
Expand Down Expand Up @@ -1098,6 +1100,8 @@ def c_validator_unregister(self) -> Any:

def multi_sig(self, multi_sig_user, inner_action, signatures, nonce, vault_address=None):
multi_sig_user = multi_sig_user.lower()
if vault_address is None:
vault_address = self.vault_address
payload_action = _multi_sig_payload_action(inner_action)
multi_sig_action = {
"type": "multiSig",
Expand All @@ -1122,6 +1126,7 @@ def multi_sig(self, multi_sig_user, inner_action, signatures, nonce, vault_addre
multi_sig_action,
signature,
nonce,
vault_address,
)

def use_big_blocks(self, enable: bool) -> Any:
Expand Down
61 changes: 60 additions & 1 deletion tests/signing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
import pytest
from eth_utils import to_hex

from hyperliquid.exchange import _multi_sig_payload_action
from hyperliquid.exchange import Exchange, _multi_sig_payload_action
from hyperliquid.utils.signing import (
MULTI_SIG_ENVELOPE_SIGN_TYPES,
OrderRequest,
ScheduleCancelAction,
action_hash,
construct_phantom_agent,
float_to_int_for_hashing,
order_request_to_order_wire,
order_wires_to_order_action,
recover_user_from_user_signed_action,
sign_l1_action,
sign_usd_transfer_action,
sign_withdraw_from_bridge_action,
Expand Down Expand Up @@ -228,6 +230,63 @@ def test_multi_sig_user_set_abstraction_payload_uses_wire_enum():
assert action["abstraction"] == "disabled"


def test_multi_sig_uses_the_same_vault_address_it_signs():
# The outer multi-sig signature commits to the vault address via action_hash, so the
# vaultAddress that goes out in the payload has to be the one that was signed.
wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123")
vault_address = "0x1719884eb866cb12b2686399699950d60822350c"
nonce = 1700000000000
inner_action = {"type": "cancel", "cancels": [{"a": 4, "o": 1}]}

posted = {}

class CapturingExchange(Exchange):
def post(self, url_path, payload=None):
posted.update(payload)
return {}

for constructor_vault, argument_vault in [
(None, vault_address),
(vault_address, None),
(vault_address, vault_address),
(None, None),
]:
posted.clear()
exchange = CapturingExchange(
wallet,
"https://api.hyperliquid-testnet.xyz",
meta={"universe": []},
spot_meta={"universe": [], "tokens": []},
vault_address=constructor_vault,
)
exchange.multi_sig(
"0x0000000000000000000000000000000000000005",
inner_action,
[],
nonce,
vault_address=argument_vault,
)

# Recompute the outer signature the way a verifier would: from what was actually posted.
action_without_tag = posted["action"].copy()
del action_without_tag["type"]
envelope = {
"multiSigActionHash": action_hash(
action_without_tag, posted["vaultAddress"], posted["nonce"], posted["expiresAfter"]
),
"nonce": posted["nonce"],
"signatureChainId": "0x66eee",
}
recovered = recover_user_from_user_signed_action(
envelope,
posted["signature"],
MULTI_SIG_ENVELOPE_SIGN_TYPES,
"HyperliquidTransaction:SendMultiSig",
False,
)
assert recovered.lower() == wallet.address.lower()


def test_create_sub_account_action():
wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123")
action = {
Expand Down