Skip to content

fix(soroban): render signed SCMap entry on the signing screens - #1012

Merged
aristidesstaffieri merged 4 commits into
mainfrom
fix/soroban-signing-display-fidelity
Oct 20, 2026
Merged

aristidesstaffieri merged 4 commits into
mainfrom
fix/soroban-signing-display-fidelity

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Renders Soroban contract-call arguments on the signing screens from the signed SCVal rather than from a decoded native value, so the screen matches what is being signed. Three defects are fixed:

  • Map entries no longer disappear. Decoding a map to a JS object coerced every key to a string and let a later entry overwrite an earlier one, so an SCMap with N signed entries could render as one line — with no glyph and no warning. Maps now render from the signed entry list, so entry count is preserved by construction.
  • Key types stay distinguishable. u64(1) and string("1"), or symbol("a") and string("a"), previously rendered as identical keys. Quoting now carries the type: strings quoted, symbols and numbers bare, structured keys inline.
  • Binary text stays distinguishable, and valid text stays readable. SCString, SCSymbol and function names are decoded strictly, falling back to the reversible SEP-51 escape form only when the bytes are not valid UTF-8. Two distinct non-UTF-8 payloads can no longer collapse onto the same U+FFFD string, and legitimate non-ASCII text is no longer escaped byte by byte (café rendered caf\xc3\xa9).

All three hold at every nesting depth. Only a top-level argument was handled before: one level down a map collapsed, a non-UTF-8 string rendered as {"0":97,"1":108,…}, and bytes as {"type":"Buffer","data":[…]}.

Affected surfaces: SignTransactionDetails (operations and authorizations) and the WalletKit dApp auth-entry display, both of which route through the shared helpers.

Display format change — map and vector parameters render as a value literal rather than JSON:

{                          {                        {
  1: "from-u64",             a: "sym",                { id: 0 }: "v0",
  "1": "from-string"         "a": "str"               { id: 1 }: "v1"
}                          }                        }
 u64 vs string key          symbol vs string key     struct keys

Non-UTF-8 strings and symbols also move from bare hex to the SEP-51 form already used for executable tags: fffe becomes \xff\xfe.

Test plan

  • yarn jest — 3283 passing, including 17 new cases: entry-count preservation for structured and mixed-type map keys, address- and u32-keyed controls, nested containers, and strict decoding of non-UTF-8 strings, symbols and function names
  • yarn lint:ts and eslint clean
  • Manual: struct-keyed, mixed-key and symbol-vs-string map arguments, plus a non-UTF-8 string argument and function name, through the signing and WalletKit auth screens
  • CI green

@aristidesstaffieri aristidesstaffieri self-assigned this Sep 21, 2026
Comment thread src/helpers/soroban.ts Fixed
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-f7e11d957e3259ac3602
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-f7df808ea508054af58a
Backend: sandbox (aristidesstaffieri). SDF collaborators only — install instructions in the release description.

…oNative

  Decoding an SCMap to a JS object coerced every key through ToPropertyKey
  and applied last-write-wins, so a map with N signed entries could render
  as one line on the approval screen. Render maps and vecs from the signed
  entry list instead, as a value literal where quoting carries the key type.

  Decode SCString, SCSymbol, executable tags and function names strictly,
  falling back to the reversible SEP-51 escape form only when the bytes are
  not valid UTF-8. Two distinct binary payloads can no longer collapse onto
  one screen string, and valid non-ASCII text stays readable instead of
  being escaped byte by byte.
…l type

  Ports the remaining two commits of stellar/freighter#3017.

  - Escape XdrString-backed fields to one prefix-free byte string: valid text
    containing `\xff` rendered like the byte 0xFF, and bidi overrides reached
    the screen raw, letting one signed string reorder into another.
  - Decode strict UTF-8 by hand -- RN's TextDecoder polyfill throws on the
    `fatal` option, which Jest never sees because Node implements it.
  - Render arguments from a token stream, so the drawn and copied text cannot
    drift, and let the signer tap a scalar to see the arm it was signed as.
  - scValByType returns a string on every arm; `false`, `void`, a ledger-key
    instance and a non-wasm instance each reached the screen as an empty row.
    Instances now render their storage map and the CAP-85 owner and tag.
  - Key the contract-spec lookup off the raw signed function name, not the
    escaped display form.
@aristidesstaffieri
aristidesstaffieri force-pushed the fix/soroban-signing-display-fidelity branch from 998a16a to 28ebcf7 Compare September 28, 2026 20:13
@aristidesstaffieri
aristidesstaffieri marked this pull request as ready for review September 28, 2026 21:02
Copilot AI lite review requested due to automatic review settings September 28, 2026 21:02
@aristidesstaffieri aristidesstaffieri changed the title fix(soroban): render signing-screen values from the SCVal, not scValT… fix(soroban): render signed SCMap entry on the signing screens Sep 28, 2026
@aristidesstaffieri
aristidesstaffieri requested a review from a team September 28, 2026 21:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Comment thread src/helpers/soroban.ts
Comment thread src/helpers/soroban.ts Outdated
punct("owner: "),
value(addressToString(ref.executableOwner), executable.type),
],
[punct("tag: "), value(xdrStringToDisplay(ref.tag), executable.type)],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An executable tag can fake extra fields, including a second owner

Merge blocker. The signing screen can show an owner address that was never signed.

TL;DR: The external-reference tag is shown on the signing screen without quotes or escaping of commas, colons or brackets. A contract-instance tag of x, owner: G…ATTACKER therefore renders as a second owner: field. The same problem lets a vector holding one tag "1, 2" look identical to a vector of two numbers.

Steps to reproduce:

  1. Build a contract-call argument containing a contract instance whose executable is an external reference with tag x, owner: <any G address>.
  2. Open it on the signing screen. It shows:
contractInstance {
  executable: externalRef {
    owner: GA46Q…ASTJD,
    tag: x, owner: GAAOG…CZBQ2
  }
}

The compact (map-key) form has the same problem.


Detailed explanation (for agents)

Root cause: in a value literal, both sites render the tag with xdrStringToDisplay, which escapes backslashes and control characters but not the structural characters , : { } [ ]. Strings and symbols already go through the quoting xdrStringToLiteral, but these two sites don't.

The separate tag row that getInvocationArgs builds for create-contract (externalRefDetails.tag) is its own labelled field, not part of a literal, so it can't fake structure this way.

Deterministic repro: call scValToDisplayValue on scvContractInstance with contractExecutableExternalRef({ executableOwner, tag: "x, owner: G…" }), and on scvMap with keys scvVec([scvExecutableTag("1, 2")]) and scvVec([scvU32(1), scvU32(2)]). The second case renders both keys as [1, 2]. Checked at 28ebcf71108beba44d1c0211f9d2147a9fc914dc against @stellar/stellar-sdk 17.0.1.

Suggested fix: render both sites with xdrStringToLiteral(…, "string") so the tag is quoted, with " escaped. If the tag needs to look different from a string, use a wrapper such as tag("…").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including this one for the same reason: small fix for correctness

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tag quoting in bc9d899

@piyalbasu piyalbasu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left 2 small comments, but otherwise LGTM

aristidesstaffieri and others added 2 commits October 20, 2026 09:54
  Port of stellar/freighter@78c6a04. An external reference's tag is now a
  quoted string literal and a standalone SCVal tag renders as tag("…"), so
  a deployer-chosen tag can neither forge an `owner:` field nor read as a
  string with the same characters.
@aristidesstaffieri
aristidesstaffieri merged commit 706db48 into main Oct 20, 2026
49 of 50 checks passed
@aristidesstaffieri
aristidesstaffieri deleted the fix/soroban-signing-display-fidelity branch October 20, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants