From c3e02919af3ffc815097822a1d31421f98156b7b Mon Sep 17 00:00:00 2001 From: BhariGowda Date: Sat, 22 Aug 2026 16:58:57 +0530 Subject: [PATCH] Raise instead of warn when multi-sig sign types cannot be enriched add_multi_sig_types inserts payloadMultiSigUser and outerSigner after the hyperliquidChain entry. When sign_types has no hyperliquidChain entry it printed a warning and returned the list unchanged, then signing carried on with the un-enriched types. eth_account builds the EIP-712 hash from the type list and ignores message keys with no matching type entry, so add_multi_sig_fields still puts payloadMultiSigUser and outerSigner into the message and they are silently dropped from the hash. The result is a signature identical to a plain single-signer one for the same action: BAD = [t for t in USD_SEND_SIGN_TYPES if t["name"] != "hyperliquidChain"] sign_multi_sig_user_signed_action_payload(w, a, False, BAD, ..., msu, outer) == sign_user_signed_action(w, a, BAD, ..., False) # True It carries no binding to the multi-sig user or the outer signer, so it is not usable as an inner multi-sig signature. All of the sign types exported from this module have hyperliquidChain first, so this only affects callers passing their own type list, which is the case for any action the SDK does not have a helper for yet. A stdout warning is easy to miss there. Raising ValueError instead. No signature that the chain accepts today changes, since the un-enriched path could not produce one. --- hyperliquid/utils/signing.py | 6 +++++- tests/signing_test.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/hyperliquid/utils/signing.py b/hyperliquid/utils/signing.py index 56041471..226df23a 100644 --- a/hyperliquid/utils/signing.py +++ b/hyperliquid/utils/signing.py @@ -273,7 +273,11 @@ def add_multi_sig_types(sign_types): } ) if not enriched: - print('"hyperliquidChain" missing from sign_types. sign_types was not enriched with multi-sig signing types') + raise ValueError( + '"hyperliquidChain" missing from sign_types, so sign_types cannot be enriched with the multi-sig ' + "signing types. Without payloadMultiSigUser and outerSigner the signature is not bound to the " + "multi-sig user or the outer signer and will not be accepted." + ) return enriched_sign_types diff --git a/tests/signing_test.py b/tests/signing_test.py index b6e89d9f..8bef99f6 100644 --- a/tests/signing_test.py +++ b/tests/signing_test.py @@ -4,14 +4,17 @@ from hyperliquid.exchange import _multi_sig_payload_action from hyperliquid.utils.signing import ( + USD_SEND_SIGN_TYPES, OrderRequest, ScheduleCancelAction, action_hash, + add_multi_sig_types, construct_phantom_agent, float_to_int_for_hashing, order_request_to_order_wire, order_wires_to_order_action, sign_l1_action, + sign_multi_sig_user_signed_action_payload, sign_usd_transfer_action, sign_withdraw_from_bridge_action, ) @@ -228,6 +231,43 @@ def test_multi_sig_user_set_abstraction_payload_uses_wire_enum(): assert action["abstraction"] == "disabled" +def test_add_multi_sig_types_rejects_types_without_hyperliquid_chain(): + # eth_account ignores message keys that have no matching entry in the EIP-712 types, so if the + # multi-sig fields are silently left out the resulting signature is byte-identical to a plain + # single-signer one and carries no binding to the multi-sig user or the outer signer. + wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") + types_without_chain = [t for t in USD_SEND_SIGN_TYPES if t["name"] != "hyperliquidChain"] + action = { + "destination": "0x5e9ee1089755c3435139848e47e6635505d5a13a", + "amount": "1", + "time": 1687816341423, + } + + with pytest.raises(ValueError): + add_multi_sig_types(types_without_chain) + + with pytest.raises(ValueError): + sign_multi_sig_user_signed_action_payload( + wallet, + action, + False, + types_without_chain, + "HyperliquidTransaction:UsdSend", + "0x0000000000000000000000000000000000000005", + wallet.address, + ) + + enriched = add_multi_sig_types(USD_SEND_SIGN_TYPES) + assert [t["name"] for t in enriched] == [ + "hyperliquidChain", + "payloadMultiSigUser", + "outerSigner", + "destination", + "amount", + "time", + ] + + def test_create_sub_account_action(): wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123") action = {