Skip to main content

2026-07-16 Protocol Discussion

· 5 min read
Kaan Kacar
Developer Advocate

This week's protocol discussion had two parts. First we wrapped up the CAP-84 debate from last week. Then Dmytro Kozhevin (Dima) presented CAP-86. Discord gave a few people audio trouble at the start, so if you could not hear Dima live, the recording above is the place to catch up.

CAP-84 Follow-Up: Why Muxed Contract Addresses​

Jake Urban, the author of CAP-84, joined to explain the product case. His core point: every valid reason to mux a G account also applies to contract accounts.

  • Omnibus accounts: Most exchanges take deposits and withdrawals on G accounts with memos or muxed addresses. If an exchange or custodian moves to smart accounts, it loses both options, because a contract invocation cannot carry a memo.
  • Per-payment IDs: On and off ramps attach an off-chain ID to a payment to reconcile it with the on-chain transfer. A muxed contract address can carry that ID in the address itself.

George restated his concern from last week. He does not have a strong objection, but he wanted the group to check if a Soroban-level pattern, such as a standard omnibus contract that emits the ID in events, could reach the same goal without a protocol change. He noted that the impact is smaller than he first thought, because integrators that read the CAP-67 unified events do not need the new strkey.

Jake's reply:

  • A contract-level pattern would give each address type a different interface, which is a footgun for developers and would slow contract account adoption.
  • Today you send a muxed deposit by calling the token contract directly. With a custom pattern, you would call the contract account, which then calls the token. That is a worse developer experience.
  • Muxed G addresses already exist, so leaving contracts out feels like going halfway.

George agreed that fragmentation is a real concern and was fine with the decision once the alternative was considered. Dima added that a single address string is much safer than an address plus a separate ID. Users lose funds when they forget the memo, which is why muxed accounts exist in the first place.

A builder in the audience gave a real example. Their team won an SDF RFP for a smart account onboarding protocol. Today, on and off ramps cannot deposit directly to a C address, so they run a relayer: deposits go to a pooled G address with a memo ID, and the relayer forwards the funds to the user's contract account. Muxed contract addresses would remove that whole layer. With that, the group considered George's question about demand mostly answered.

CAP-86: Sparse Map Reads and Writes​

CAP-86 fixes a problem that Soroban has had since launch: you cannot change a #[contracttype] struct without breaking every reader of that struct.

The SDK uses fast host functions to convert structs to and from maps. These functions require every requested field to exist, and every field in the map to be consumed. If you add, remove or rename a field, the reader traps. The contract cannot catch the error and try another layout. People have hit this in practice: they upgraded a contract, added a field to the struct that holds the admin, and could no longer read that struct, so they could not upgrade again.

The CAP adds two host functions, sparse_map_new_from_linear_memory and sparse_map_unpack_to_linear_memory:

  • Sparse reads: The reader names the fields it wants. The map can have more fields than requested. A missing field comes back as Void, which the SDK can map to Option::None. If you need a field, make it non-optional and the read fails as expected when the field is missing.
  • Sparse writes: A None field is not written to the map at all. This saves storage and makes event payloads with many optional fields smaller. Dima mentioned SEP-41 events with an attached map as one use.

This enables migrations that were not possible before. You can add an optional field and fill a default when it is None, remove fields you no longer need, and extend structs passed between contracts without new functions. Dima compared it to protobuf v3, where every field is optional and the reader decides what is required.

Caveats:

  • If two contracts disagree on a field name, an optional field could silently read as None. This needs a miscommunication and an optional field, so Dima considers it a small risk compared to the one it removes.
  • Old readers break if a writer starts omitting None fields, because they expect every field. The SDK will decide when to use sparse writes.
  • The CAP adds new functions and does not change existing ones, so no deployed contract changes behavior. To use the new semantics, you must update the SDK and rebuild your contract.

Performance is the same as the current functions, or slightly faster when you skip fields.

Please add your comments to the CAP-84 and CAP-86 discussion threads.