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:
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
userSetAbstraction fails as a multisig inner action in the attached
public testnet reproductions.
- The same failure also appears in our local backend E2E path.
- The outer signature and inner signatures from the rejected flow recover
to the expected addresses under the tested envelopes.
- The issue
#177 ordering idea and the PR #269 outer-hash idea do not
resolve this action in our tests.
- In our local environment, the same multisig + outer-signer pipeline
succeeds for withdraw3.
- 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
- Is
userSetAbstraction currently supported as a multisig inner action on
testnet?
- If yes, what is the canonical structure the server expects when validating
the outer signer for a multiSig-wrapped userSetAbstraction?
- If there is an action-specific canonicalization rule for this path, is it
documented anywhere outside the server implementation?
- 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
userSetAbstractionviamultiSigon testnet returnsInvalid multi-sig outer signerSuggested issue title
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:
userSetAbstractionis supported as a multisig inner action on testnetPython SDK helpers
Environment
hyperliquid-python-sdk0.22.03.14.3https://api.hyperliquid-testnet.xyz)POST /exchange0x3646A70A3E07A4152c3482384662B8255CA3BA2A0x0940Eb20ff9FC513eA156e3d6964b53a13D3f3Ac0x7DD9bE4892717A4B10450E231b8e9A7F89126E9a2 of 6signatureChainId0x66eeehyperliquidChainTestnetObserved facts
scripts/sdk_native_multisig_clean.py+logs/sdk-native-clean.logHTTP 200, body{"status":"err","response":"Invalid multi-sig outer signer"}#177ordering variantscripts/sdk_native_multisig_chain_first.py+logs/chain-first-test.logHTTP 200, body{"status":"err","response":"Invalid multi-sig outer signer"}#269patched outer-hash variantscripts/sdk_native_multisig_with_pr269_fix.py+logs/pr269-patched.logHTTP 200, body{"status":"err","response":"Invalid multi-sig outer signer"}1776333821309../e2e-output.log+../container-logs-full.log{"status":"err","response":"Invalid multi-sig outer signer"}and the abstraction mode stayeddefault -> default../recover_output.log0x0940...under the tested testnet envelope variants../recover_inner_output.logwithdraw3for5.0USDC, nonce/time1776338657078{'status': 'ok', 'response': {'type': 'default'}}; no Hyperliquid explorer hash was persisted in our artifacts for this controlwithdraw3for1.0USDC, nonce/time1776338975894{'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:
Observed output:
Alternative repro that still fails after applying the PR
#269hypothesis in-memory:
Observed output:
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/exchangeNo project-specific backend, proxy, or Chainstack endpoint is involved in
those reproductions.
2. The issue
#177field-ordering idea does not resolve this actionWe tested the variant where chain fields are moved immediately after
typein 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.pylogs/chain-first-test.logObserved result:
Invalid multi-sig outer signer3. The PR
#269outer-hash idea does not resolve this actionWe monkey-patched
sign_multi_sig_actionto preserve"type"in theaction_hashinput, following the proposal inPR #269.
Artifact:
scripts/sdk_native_multisig_with_pr269_fix.pylogs/pr269-patched.logObserved result:
Invalid multi-sig outer signer4. The outer signature recovers to the expected outer signer
Artifact:
../recover_output.logObserved matches include:
5. The inner signatures recover to the expected signers when the multisig-enriched EIP-712 types are used
Artifact:
../recover_inner_output.logObserved matches include:
Observed misses include:
In the captured rejected flow, the recovered inner signers match only when
the multisig-enriched EIP-712 types are used; the
base types onlycontrol 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, nonce1776333821309:Invalid multi-sig outer signerwithdraw3, amount5.0, nonce/time1776338657078:{'status': 'ok', 'response': {'type': 'default'}}withdraw3, amount1.0, nonce/time1776338975894:{'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/timevalues, the action payload, and the Hyperliquid APIresponse. 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
withdraw3control with nonce/time
1776338657078.The exact API log line for the successful control was:
The exact API log line for the failing action was:
The exact API log line for the fee control was:
What this report does and does not claim
What is directly supported by the attached evidence
userSetAbstractionfails as a multisig inner action in the attachedpublic testnet reproductions.
to the expected addresses under the tested envelopes.
#177ordering idea and the PR#269outer-hash idea do notresolve this action in our tests.
succeeds for
withdraw3.withdraw3control does notinclude a Hyperliquid transaction hash, so the strict reference we can
provide is
nonce/time+ action payload + API response, not anexplorer URL.
What we are not asserting
an undocumented action-specific rule.
userSetAbstractionis supposed to besupported as a multisig inner action on testnet.
Questions for maintainers
userSetAbstractioncurrently supported as a multisig inner action ontestnet?
the outer signer for a
multiSig-wrappeduserSetAbstraction?documented anywhere outside the server implementation?
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
userSetAbstractionpath in our integrationfor 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