Skip to content

Sui: server-side fee estimation (/wallet/v1/account/estimate-fee) overrides the transaction's sender with accountAddress, blocking all multisig / sender≠account signing #12534

Description

@freddi8e

Environment

  • OneKey Desktop 6.5.0 (macOS arm64, in-app dApp browser)
  • Hardware: OneKey device, firmware 4.21.0 (hardware itself signs the tx correctly — verified separately)
  • dApp: Sagat multisig (sagat.mystenlabs.com), Sui mainnet
  • dApp SDK: @mysten/sui 2.16.0, via wallet-standard sui:signTransaction

Summary

When a dApp asks OneKey to sign a Sui transaction whose sender is not the connected account (the standard multisig flow: sender = the multisig address, signer = one member), the sign modal gets stuck at fee estimation with a node error.

Root cause (pinned by direct API probing, see below): on non-custom networks the client does no local dry-run; it POSTs to /wallet/v1/account/estimate-fee with accountAddress = connected account. The backend then simulates the transaction using accountAddress as the sender, ignoring the sender embedded in encodedTx.rawTx. The client-side code paths all preserve the embedded sender correctly — the swap happens server-side.

For transactions using SIP-58 FundsWithdrawal (withdrawFrom: Sender), this is fatal: the withdrawal is resolved against the member's (empty) address balance instead of the multisig's, and the node rejects it:

Error checking transaction input objects: Invalid withdraw reservation:
Available amount in account for object id 0xa0391889481986c37fe2a170a9fe2ed9835a47a3aac74d0f8ca5ce97ae993afc
is less than requested: 0 < 1

Steps to reproduce

  1. Create a Sui multisig (threshold N) where one member key lives on a OneKey device; fund the multisig's address balance (SIP-58 funds, e.g. USDC) — not Coin objects.
  2. In Sagat, create a proposal that spends the address balance (0x2::balance::redeem_funds + send_funds, input FundsWithdrawal { withdrawFrom: Sender }).
  3. Open Sagat in OneKey Desktop 6.5.0's in-app browser, connect the member account, and try to sign the proposal.

Expected: OneKey signs the exact transaction (sender = multisig 0x7a44c9…).
Actual: error above; signing is blocked.

Evidence that the sender was swapped

The accumulator object id in the error is a deterministic fingerprint of (address, coin type):

  • (multisig 0x7a44c99ce19af6d7…, USDC)0x4a237736613e2e14… (holds 9000 base units)
  • (member 0x182c1fe77c44a1f3…, USDC)0xa0391889481986c3… ← the id in the error (holds 0)

So the checked transaction's sender was the member, not the multisig. Reproducible locally:

// dry-run the same PTB with sender = member → byte-identical error, same object id
// dry-run with sender = multisig            → { status: "success" }

Meanwhile the dApp-side transaction is provably correct: the proposal's stored transactionBytes decode to sender = 0x7a44c9… (multisig) and dry-run successfully as-is. The requested amount is also unchanged (1 base unit in both), so only the sender differs.

Root cause: pinned by probing the estimate-fee API directly

We replayed the exact payload the desktop client sends (rawTx = the dApp transaction serialized via toJSON, with sender = multisig embedded) against POST https://wallet.onekeycn.com/wallet/v1/account/estimate-fee, varying only the two sender-ish fields:

# accountAddress encodedTx.sender embedded sender in rawTx result
A (what the client sends) member member multisig the exact error above
B multisig multisig multisig Success
C member multisig multisig the exact error above
D multisig member multisig Success

C vs D is conclusive: the backend takes the simulation sender from accountAddress and ignores both encodedTx.sender and the sender embedded in rawTx.

Client-side paths are all correct — we traced the 6.5.0 app.asar end to end and every step preserves an existing sender (inpage toJSON, ProviderApiSui uses the connected address only as a fallback, keyring toTransaction uses setSenderIfNotSet, the local estimateFeeByRpc prefers getData().sender). But estimateFeeByRpc only runs for custom networks; on mainnet estimateFeeByApi is used and the server-side override above kicks in, so the sign modal errors out before the user can ever sign.

Suggested fix: the backend should use the sender embedded in the transaction when present, falling back to accountAddress only for sender-less transactions (the client-side estimateFeeByRpc already implements exactly this precedence). Alternatively, the client could fall back to local RPC estimation when tx.sender ≠ accountAddress.

Note: this is unrelated to the (already fixed) 6.4.0 issue where the bundled @mysten/sui 1.28.0 could not parse FundsWithdrawal at all (Invalid type: Expected Object but received Object / Unknown value 2 for enum CallArg). 6.5.0 parses the transaction fine — this report is about the sender being replaced afterwards.

Impact

Any flow where the tx sender ≠ connected account fails at the fee-estimation step — most importantly every Sui multisig (Sagat or otherwise), on Desktop and (since it shares the same background code and API) presumably the extension as well. Because the fix is server-side, no client release is needed for the main issue.

Related: is there a plan to ship 6.5.0 to the Chrome extension? The Web Store still serves 6.4.0 (listing last updated Jun 16), whose bundled @mysten/sui 1.28.0 cannot parse FundsWithdrawal transactions at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions