Context
When a dapp calls addToken({ contractId }) and the resolved contract is a Stellar Asset Contract (SAC) — i.e. the deterministic Soroban wrapper around a classic Stellar asset — Freighter's /add-token confirm flow signs and submits a changeTrust operation for the underlying classic asset (useSetupAddTokenFlow → useChangeTrustline → signFreighterTransaction → submitFreighterTransaction).
For pure-Soroban (SEP-41) tokens — contract-id issuer, no classic counterpart — the same popup path only writes the token to the local "tracked" list; nothing is signed and nothing is broadcast.
The current popup uses identical copy and layout for both cases — the SAC branch never tells the user that a Stellar transaction will be signed, that a trustline reserve will be locked, or that a network fee will be charged. Separately, in both cases the popup omits the token's contract address, so users can't cross-check that Freighter is registering the same contract the dapp claimed it was registering.
This issue tracks tightening the disclosure in the /add-token popup so the user can see (a) the contract being registered, and (b) for the SAC branch, what is actually being signed on-chain.
Current design (production today)

What the user sees today for a SAC token:
- Generic description: "Allow token to be displayed and used with this wallet address"
- One metadata row: Wallet
- No contract address, no issuer, no reserve, no fee
What's actually about to happen when they hit Confirm:
changeTrust({ asset: new Asset(code, issuer) }) op gets built against the resolved classic issuer
- The user's account is the source; their session-unlocked key signs it
- The signed XDR is
POSTed to the indexer's /submit-tx
- A 0.5 XLM subentry reserve is locked, plus a network fee
For pure-Soroban tokens the popup is identical, even though only the local tracked-token list is touched. The token's self-reported name/symbol come from the contract's own name()/symbol() calls — strings the contract author controls — so there's no on-screen identifier the user can use to verify what's actually being registered.
Proposed design (AI-generated mock)

The mock above includes:
-
Contract row — shown in both branches (SEP-41 and SAC). The contract address (params.contractId, truncated) is the only un-spoofable on-screen identifier, so it should be displayed even for the SEP-41 case where nothing is signed. Lets the user cross-check Freighter's resolved contract against whatever the dapp told them.
-
SAC-only disclosure block — rendered only when StrKey.isValidEd25519PublicKey(assetIssuer) === true:
- Replacement description copy instead of the generic display-only line:
"Approving will submit a Stellar transaction that opens a trustline to this asset's issuer. This will modify your Stellar account, lock a reserve, and charge a network fee."
- Three additional metadata rows below Contract:
- Issuer — truncated G… address of the underlying classic issuer
- Account reserve —
0.5 XLM (from BASE_RESERVE)
- Network fee — the same
recommendedFee value that flows into getManageAssetXDR (so what's displayed matches what's actually charged)
The pure-Soroban branch keeps the existing description and adds only the Contract row. SAC tokens get Contract + the disclosure block.
(The mock is a working prototype, not a final design — handing off for adjustments. The "Not on your lists" banner in both screenshots is an existing component, not part of this change.)
Open design questions
- Icon choices for the new rows (current mock:
Icon.CodeCircle01 for Contract, Icon.User01 for Issuer, Icon.Lock01 for Account reserve, Icon.Coins03 for Network fee)
- Row order — current is Wallet → Contract → Issuer → Reserve → Fee. Should the on-chain rows be visually grouped/separated from Wallet + Contract (which are present on every token)?
- Wording on Account reserve — "Trustline reserve" may communicate the locked-not-spent nature more clearly
- Whether the description block needs additional visual weight (info or warning icon) on the SAC branch
- Whether Contract should sit above or below Wallet (current: below)
Implementation
Draft PR: #2827 (branch chore/addtoken-sac-disclosure). Two commits, presentational only — no changes to useSetupAddTokenFlow, useChangeTrustline, or any signing path:
a9c5ed7f — SAC disclosure block (description copy + Issuer / Reserve / Fee rows)
4c1ac5ec — unconditional Contract row
Held as draft until design direction is confirmed; ping me on the PR once we land on copy + layout adjustments and I'll iterate.
Context
When a dapp calls
addToken({ contractId })and the resolved contract is a Stellar Asset Contract (SAC) — i.e. the deterministic Soroban wrapper around a classic Stellar asset — Freighter's/add-tokenconfirm flow signs and submits achangeTrustoperation for the underlying classic asset (useSetupAddTokenFlow→useChangeTrustline→signFreighterTransaction→submitFreighterTransaction).For pure-Soroban (SEP-41) tokens — contract-id issuer, no classic counterpart — the same popup path only writes the token to the local "tracked" list; nothing is signed and nothing is broadcast.
The current popup uses identical copy and layout for both cases — the SAC branch never tells the user that a Stellar transaction will be signed, that a trustline reserve will be locked, or that a network fee will be charged. Separately, in both cases the popup omits the token's contract address, so users can't cross-check that Freighter is registering the same contract the dapp claimed it was registering.
This issue tracks tightening the disclosure in the
/add-tokenpopup so the user can see (a) the contract being registered, and (b) for the SAC branch, what is actually being signed on-chain.Current design (production today)
What the user sees today for a SAC token:
What's actually about to happen when they hit Confirm:
changeTrust({ asset: new Asset(code, issuer) })op gets built against the resolved classic issuerPOSTed to the indexer's/submit-txFor pure-Soroban tokens the popup is identical, even though only the local tracked-token list is touched. The token's self-reported
name/symbolcome from the contract's ownname()/symbol()calls — strings the contract author controls — so there's no on-screen identifier the user can use to verify what's actually being registered.Proposed design (AI-generated mock)
The mock above includes:
Contract row — shown in both branches (SEP-41 and SAC). The contract address (
params.contractId, truncated) is the only un-spoofable on-screen identifier, so it should be displayed even for the SEP-41 case where nothing is signed. Lets the user cross-check Freighter's resolved contract against whatever the dapp told them.SAC-only disclosure block — rendered only when
StrKey.isValidEd25519PublicKey(assetIssuer) === true:0.5 XLM(fromBASE_RESERVE)recommendedFeevalue that flows intogetManageAssetXDR(so what's displayed matches what's actually charged)The pure-Soroban branch keeps the existing description and adds only the Contract row. SAC tokens get Contract + the disclosure block.
(The mock is a working prototype, not a final design — handing off for adjustments. The "Not on your lists" banner in both screenshots is an existing component, not part of this change.)
Open design questions
Icon.CodeCircle01for Contract,Icon.User01for Issuer,Icon.Lock01for Account reserve,Icon.Coins03for Network fee)Implementation
Draft PR: #2827 (branch
chore/addtoken-sac-disclosure). Two commits, presentational only — no changes touseSetupAddTokenFlow,useChangeTrustline, or any signing path:a9c5ed7f— SAC disclosure block (description copy + Issuer / Reserve / Fee rows)4c1ac5ec— unconditional Contract rowHeld as draft until design direction is confirmed; ping me on the PR once we land on copy + layout adjustments and I'll iterate.