Skip to content

userSetAbstraction via multiSig on testnet/mainnet returns Invalid multi-sig outer signer #291

@alfredolopez80

Description

@alfredolopez80

userSetAbstraction via multiSig on testnet returns Invalid multi-sig outer signer

Suggested issue title

Testnet: userSetAbstraction via multiSig returns Invalid multi-sig outer signer; same multisig/outer-signer pipeline succeeds for withdraw3

Scope of this report

This report is intentionally limited to observed facts and attached
artifacts.

We are not asserting the root cause. We are reporting the exact
behaviors we reproduced and asking whether:

  • userSetAbstraction is supported as a multisig inner action on testnet
  • there is an action-specific canonicalization rule not reflected in the
    Python SDK helpers

Environment

Item Value
SDK version hyperliquid-python-sdk 0.22.0
Python 3.14.3
Network Testnet (https://api.hyperliquid-testnet.xyz)
Direct public endpoint tested POST /exchange
Multisig user 0x3646A70A3E07A4152c3482384662B8255CA3BA2A
Outer signer used in direct repros 0x0940Eb20ff9FC513eA156e3d6964b53a13D3f3Ac
Second signer used in local controls 0x7DD9bE4892717A4B10450E231b8e9A7F89126E9a
Threshold 2 of 6
signatureChainId 0x66eee
hyperliquidChain Testnet

Observed facts

# Scenario Artifact Observed result
1 Direct public repro, clean current script scripts/sdk_native_multisig_clean.py + logs/sdk-native-clean.log HTTP 200, body {"status":"err","response":"Invalid multi-sig outer signer"}
2 Direct public repro, issue #177 ordering variant scripts/sdk_native_multisig_chain_first.py + logs/chain-first-test.log HTTP 200, body {"status":"err","response":"Invalid multi-sig outer signer"}
3 Direct public repro, PR #269 patched outer-hash variant scripts/sdk_native_multisig_with_pr269_fix.py + logs/pr269-patched.log HTTP 200, body {"status":"err","response":"Invalid multi-sig outer signer"}
4 Local backend E2E with the same multisig action, nonce 1776333821309 ../e2e-output.log + ../container-logs-full.log execution failed with {"status":"err","response":"Invalid multi-sig outer signer"} and the abstraction mode stayed default -> default
5 Outer signature recovery from a rejected payload ../recover_output.log recovered address matches expected outer signer 0x0940... under the tested testnet envelope variants
6 Inner signature recovery from the same rejected flow ../recover_inner_output.log both inner signatures recover to the expected signers when multisig-enriched types are used
7 Positive control in the same integration environment: withdraw3 for 5.0 USDC, nonce/time 1776338657078 local Docker API logs + persisted transaction metadata (validated on 2026-04-16) Hyperliquid response {'status': 'ok', 'response': {'type': 'default'}}; no Hyperliquid explorer hash was persisted in our artifacts for this control
8 Negative fee control in the same integration environment: withdraw3 for 1.0 USDC, nonce/time 1776338975894 local Docker API logs + persisted transaction metadata (validated on 2026-04-16) Hyperliquid response {'status': 'err', 'response': 'Withdrawal is smaller than fee.'}

Minimal public reproduction

Any of the three attached scripts reproduces the same error directly
against the public testnet API. The shortest one to start with is:

pip install hyperliquid-python-sdk==0.22.0
python3 scripts/sdk_native_multisig_chain_first.py

Observed output:

HTTP 200
Body: {
  "status": "err",
  "response": "Invalid multi-sig outer signer"
}

Alternative repro that still fails after applying the PR #269
hypothesis in-memory:

python3 scripts/sdk_native_multisig_with_pr269_fix.py

Observed output:

HTTP 200
Body: {
  "status": "err",
  "response": "Invalid multi-sig outer signer"
}

Concrete validations behind the direct repros

1. The public repros do not go through our backend

The three attached scripts send their payloads directly to:

  • https://api.hyperliquid-testnet.xyz/exchange

No project-specific backend, proxy, or Chainstack endpoint is involved in
those reproductions.

2. The issue #177 field-ordering idea does not resolve this action

We tested the variant where chain fields are moved immediately after
type in the inner action:

{
    "type": "userSetAbstraction",
    "signatureChainId": "0x66eee",
    "hyperliquidChain": "Testnet",
    "user": multi_sig_user.lower(),
    "abstraction": "unifiedAccount",
    "nonce": timestamp,
}

Artifact:

  • scripts/sdk_native_multisig_chain_first.py
  • logs/chain-first-test.log

Observed result:

  • same Invalid multi-sig outer signer

3. The PR #269 outer-hash idea does not resolve this action

We monkey-patched sign_multi_sig_action to preserve "type" in the
action_hash input, following the proposal in
PR #269.

Artifact:

  • scripts/sdk_native_multisig_with_pr269_fix.py
  • logs/pr269-patched.log

Observed result:

  • same Invalid multi-sig outer signer

4. The outer signature recovers to the expected outer signer

Artifact:

  • ../recover_output.log

Observed matches include:

[MATCH] baseline (3 fields, chainId=421614)                -> 0x0940Eb20ff9FC513eA156e3d6964b53a13D3f3Ac
[MATCH] baseline chainId=int(0x66eee)=421614               -> 0x0940Eb20ff9FC513eA156e3d6964b53a13D3f3Ac

5. The inner signatures recover to the expected signers when the multisig-enriched EIP-712 types are used

Artifact:

  • ../recover_inner_output.log

Observed matches include:

[✅ MATCH] our enriched SDK order (6 fields) -> 0x0940Eb20ff9FC513eA156e3d6964b53a13D3f3Ac
[✅ MATCH] our enriched SDK order (6 fields) -> 0x7DD9bE4892717A4B10450E231b8e9A7F89126E9a

Observed misses include:

[❌ MISS ] base types only (no multisig enrichment) -> ...

In the captured rejected flow, the recovered inner signers match only when
the multisig-enriched EIP-712 types are used; the base types only
control does not match.

Additional control from our integration environment

This is not the minimal public repro, but it was useful as a control
because it exercised the same local multisig pipeline with the same
multisig and outer signer.

Validated on 2026-04-16:

  • userSetAbstraction, nonce 1776333821309:
    Invalid multi-sig outer signer
  • withdraw3, amount 5.0, nonce/time 1776338657078:
    {'status': 'ok', 'response': {'type': 'default'}}
  • withdraw3, amount 1.0, nonce/time 1776338975894:
    {'status': 'err', 'response': 'Withdrawal is smaller than fee.'}

We are intentionally not using our internal transaction IDs as the primary
reference in this report. For these local controls, the stable
cross-system identifiers available in our artifacts are the exact
nonce/time values, the action payload, and the Hyperliquid API
response. No Hyperliquid explorer hash or explorer URL was persisted in
our current integration artifacts for these three controls.

If maintainers want to inspect the successful local control on the
Hyperliquid side, the reference to look up is the testnet withdraw3
control with nonce/time 1776338657078.

The exact API log line for the successful control was:

Response: {'status': 'ok', 'response': {'type': 'default'}}

The exact API log line for the failing action was:

Hyperliquid returned error: Invalid multi-sig outer signer

The exact API log line for the fee control was:

Response: {'status': 'err', 'response': 'Withdrawal is smaller than fee.'}

What this report does and does not claim

What is directly supported by the attached evidence

  1. userSetAbstraction fails as a multisig inner action in the attached
    public testnet reproductions.
  2. The same failure also appears in our local backend E2E path.
  3. The outer signature and inner signatures from the rejected flow recover
    to the expected addresses under the tested envelopes.
  4. The issue #177 ordering idea and the PR #269 outer-hash idea do not
    resolve this action in our tests.
  5. In our local environment, the same multisig + outer-signer pipeline
    succeeds for withdraw3.
  6. In our current artifacts, the successful withdraw3 control does not
    include a Hyperliquid transaction hash, so the strict reference we can
    provide is nonce/time + action payload + API response, not an
    explorer URL.

What we are not asserting

  • We are not asserting whether the root cause is in the SDK, the server, or
    an undocumented action-specific rule.
  • We are not asserting whether userSetAbstraction is supposed to be
    supported as a multisig inner action on testnet.

Questions for maintainers

  1. Is userSetAbstraction currently supported as a multisig inner action on
    testnet?
  2. If yes, what is the canonical structure the server expects when validating
    the outer signer for a multiSig-wrapped userSetAbstraction?
  3. If there is an action-specific canonicalization rule for this path, is it
    documented anywhere outside the server implementation?
  4. If this action is not supported in the multisig path, could the SDK and/or
    docs reject it explicitly instead of allowing the flow to reach
    Invalid multi-sig outer signer?

Current workaround on our side

We have disabled the multisig userSetAbstraction path in our integration
for now.


Reporter context: Reported by the HyperSig development team, a
production multisig builder for Hyperliquid. Self-reported operational
context: HyperSig currently supports more than 400 TVL worth of multisig
activity on Hyperliquid.
Prepared by: Alfredo Lopez (alfredo@palmeradao.xyz)
Last reviewed: 2026-04-16

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions