Stellar Atlas
Protocol
CAP-0084

Muxed Contract Addresses

AcceptedGitHub

Specification

Preamble

CAP: 0084
Title: Muxed Contract Addresses
Working Group:
    Owner: Jake Urban <jake@stellar.org>
    Authors: Jake Urban <jake@stellar.org>
    Consulted: Dmytro Kozhevin <@dmkozh>, Leigh McCulloch <@leighmcculloch>
Status: Accepted
Created: 2026-06-24
Discussion: https://github.com/stellar/stellar-protocol/discussions/1950
Protocol version: TBD

Simple Summary

Extend the address multiplexing introduced for Stellar accounts in CAP-67 to contract addresses. A contract address may be paired with a uint64 multiplexing identifier so that a single on-chain contract can represent an arbitrary number of off-chain, 'virtual' balances, in the same way that a single Stellar account can today.

Working Group

As described in the preamble section.

Motivation

CAP-67 added multiplexing support for account addresses by introducing the SC_ADDRESS_TYPE_MUXED_ACCOUNT variant of SCAddress, the MuxedAddressObject host object, and a convention for representing the multiplexing identifier in the unified asset events. It did not, however, add an equivalent representation for contract addresses.

As a result there is an asymmetry in the protocol: a single Stellar account can be multiplexed to represent many off-chain destinations, but a single contract address cannot. There is no protocol-level way to attach a multiplexing identifier to a contract address, so the only way to distinguish between multiple off-chain balances held behind a contract is to deploy a distinct contract for each balance.

This CAP removes that asymmetry by replicating the CAP-67 account multiplexing design for contract addresses.

Goals Alignment

This CAP is aligned with the following Stellar Network Goals:

  • The Stellar Network should make it easy for developers of Stellar projects to create highly usable products.

Abstract

A new SC_ADDRESS_TYPE_MUXED_CONTRACT variant is added to SCAddress, consisting of a contract id and a uint64 multiplexing identifier. The existing MuxedAddressObject host object and its accessor host functions are generalized to represent muxed contract addresses in addition to muxed account addresses, requiring no new host functions. The Stellar Asset Contract transfer function accepts a muxed contract address as the destination and emits the multiplexing identifier using the existing to_muxed_id event convention. A corresponding strkey encoding for muxed contract addresses is proposed here and will be defined normatively in SEP-23.

Specification

XDR changes

This patch of XDR changes is based on the latest protocol 27 XDR files in stellar-xdr. The exact base commit will be pinned when the XDR is agreed on in a protocol meeting.

diff --git a/Stellar-contract.x b/Stellar-contract.x
--- a/Stellar-contract.x
+++ b/Stellar-contract.x
@@
 enum SCAddressType
 {
     SC_ADDRESS_TYPE_ACCOUNT = 0,
     SC_ADDRESS_TYPE_CONTRACT = 1,
     SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2,
     SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3,
-    SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4
+    SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4,
+    SC_ADDRESS_TYPE_MUXED_CONTRACT = 5
 };

 struct MuxedEd25519Account
 {
     uint64 id;
     uint256 ed25519;
 };

+struct MuxedContract
+{
+    uint64 id;
+    ContractID contractId;
+};
+
 union SCAddress switch (SCAddressType type)
 {
 case SC_ADDRESS_TYPE_ACCOUNT:
     AccountID accountId;
 case SC_ADDRESS_TYPE_CONTRACT:
     ContractID contractId;
 case SC_ADDRESS_TYPE_MUXED_ACCOUNT:
     MuxedEd25519Account muxedAccount;
 case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE:
     ClaimableBalanceID claimableBalanceId;
 case SC_ADDRESS_TYPE_LIQUIDITY_POOL:
     PoolID liquidityPoolId;
+case SC_ADDRESS_TYPE_MUXED_CONTRACT:
+    MuxedContract muxedContract;
 };

No new host functions are introduced by this CAP. The existing get_address_from_muxed_address and get_id_from_muxed_address host functions added in CAP-67 are reused, with their behavior extended to muxed contract addresses as described in the Semantics section.

Semantics

MuxedAddressObject host object

CAP-67 introduced the MuxedAddressObject host object and a 1:n mapping between SCVal::SCV_ADDRESS (and the respective SCAddress) and the host objects. This CAP extends that mapping so that SC_ADDRESS_TYPE_MUXED_CONTRACT is also represented by MuxedAddressObject:

  • (unchanged) SC_ADDRESS_TYPE_ACCOUNT and SC_ADDRESS_TYPE_CONTRACT correspond to AddressObject
  • (unchanged) SC_ADDRESS_TYPE_MUXED_ACCOUNT corresponds to MuxedAddressObject
  • SC_ADDRESS_TYPE_MUXED_CONTRACT corresponds to MuxedAddressObject
  • (unchanged) SC_ADDRESS_TYPE_CLAIMABLE_BALANCE and SC_ADDRESS_TYPE_LIQUIDITY_POOL are disallowed by the host

As with muxed account addresses, MuxedAddressObject is a regular host object and is not implicitly compatible with the AddressObject type. A contract that expects an AddressObject as an input argument will fail if an SCAddress::SC_ADDRESS_TYPE_MUXED_CONTRACT is passed to it, in the same way it fails when a muxed account address is passed.

The two host functions introduced in CAP-67 operate on a muxed contract address as follows:

  • get_address_from_muxed_address returns the contract address part as an AddressObject of type SC_ADDRESS_TYPE_CONTRACT, stripping the multiplexing identifier.
  • get_id_from_muxed_address returns the multiplexing identifier as a U64Val.

SC_ADDRESS_TYPE_MUXED_CONTRACT is prohibited in storage keys

As with SC_ADDRESS_TYPE_MUXED_ACCOUNT, a muxed contract address is meant to support off-chain multiplexing and would by definition be a mistake to use in a contract data storage key. The Soroban host will produce an error if SC_ADDRESS_TYPE_MUXED_CONTRACT is present in a contract data key SCVal (either directly, or anywhere in the nested containers). This restriction is applied to all kinds of contract data storage, including instance storage.

As with muxed account addresses, a contract that wants to use the muxing information on-chain can decompose the muxed contract address into its two parts using the provided host functions and store them in a user-defined data structure.

Update the SAC transfer function to support muxed contract addresses

The transfer function of the Stellar Asset Contract already accepts an AddressObject or MuxedAddressObject for the to argument (see CAP-67). This CAP requires no change to the function signature: a MuxedAddressObject that wraps a muxed contract address is now a valid input for the to argument. If a muxed contract address is passed, the function behaves as if the corresponding non-muxed contract AddressObject had been passed, with the only difference being the event payload as described below.

Events

This CAP introduces no new event format. The transfer and mint events emitted by the Stellar Asset Contract represent a muxed contract destination using exactly the convention defined in CAP-67: the non-muxed contract address appears in the event topics as to, and the multiplexing identifier is carried in the data map as to_muxed_id.

The transfer event format involving a muxed contract destination is topics: ["transfer", from:<non-muxed SCAddress>, to:<non-muxed contract SCAddress>] data: { amount: i128, to_muxed_id:u64 }. The mint event format is topics: ["mint", to:<non-muxed contract SCAddress>] data: { amount: i128, to_muxed_id:u64 }. As with the id field of MuxedContract, to_muxed_id is always represented as an SCV_U64.

A muxed contract destination can only originate from an invocation of the Stellar Asset Contract transfer function; a mint event carries a muxed contract destination only when the issuer is the source of such a transfer, following the issuer semantics defined in CAP-67. Consequently, none of the classic-to-event mapping rules in CAP-67 (such as deriving to_muxed_id from the transaction memo) apply to muxed contract addresses.

Strkey encoding

A new strkey type is required to provide a textual encoding for muxed contract addresses, analogous to the muxed account strkey (M...). The normative definition will be added to SEP-23; this section describes the intended encoding so that implementers can begin work.

A muxed contract strkey follows the standard strkey structure used for all other types: the byte sequence [version byte][payload][2-byte CRC16 checksum] is base32-encoded to produce the textual representation. The payload for a muxed contract address is the 32-byte contract id concatenated with the 8-byte (big-endian) multiplexing identifier, for a total payload length of 40 bytes, mirroring the muxed account strkey payload (32-byte ed25519 key followed by the 8-byte identifier).

The version byte (and therefore the resulting human-readable prefix letter) will be allocated by SEP-23. For example, a muxed contract address with contract id 0x0000...0000 (32 bytes) and multiplexing identifier 0 has a 40-byte payload of all zero bytes; this is prefixed with the version byte and suffixed with the CRC16 checksum of [version byte][payload], and the resulting 43-byte sequence is base32-encoded.

Design Rationale

Parity with CAP-67

The design deliberately mirrors the account multiplexing design from CAP-67 rather than introducing a new mechanism. Reusing the MuxedAddressObject host object, the existing accessor host functions, and the to_muxed_id event convention means that SDKs, indexers, and other downstream consumers handle muxed contract addresses with the same machinery they already use for muxed account addresses. The only genuinely new artifacts are the SCAddress variant and the strkey encoding.

Reusing MuxedAddressObject rather than adding a new host object

Because the two host functions that operate on MuxedAddressObject (get_address_from_muxed_address and get_id_from_muxed_address) already return the generic AddressObject and U64Val respectively, they apply to muxed contract addresses without modification. Introducing a separate host object for muxed contracts would duplicate this surface for no functional benefit.

Destination-only support

This CAP only enables a muxed contract address to be used as the destination of a transfer, matching CAP-67, which only emits multiplexing information for the destination (to_muxed_id) and never for the source of a transfer. Multiplexing the source of a transfer is not part of CAP-67 and is intentionally out of scope here; it can be added by a future CAP if a need arises.

Limiting scope to transfer

The Stellar Asset Contract transfer function is the same function that CAP-67 updated to accept a muxed account destination. Extending support to additional functions that take a destination argument, such as mint and transfer_from, was considered but deferred to keep contract multiplexing symmetric with account multiplexing. Should a future need arise, support can be added by a subsequent CAP.

Strkey defined in SEP-23

Strkey encodings are defined normatively in SEP-23, so the authoritative version byte, prefix letter, and validation rules for muxed contract addresses belong there rather than in this CAP. This CAP describes the intended encoding so that the strkey can be implemented in parallel, but defers the final definition to a companion SEP-23 change.

Protocol Upgrade Transition

On the protocol upgrade, the SC_ADDRESS_TYPE_MUXED_CONTRACT variant becomes a valid SCAddress, the MuxedAddressObject host object and its accessor host functions begin accepting muxed contract addresses, and the Stellar Asset Contract transfer function begins accepting a muxed contract address as the destination. The new host behavior uses the standard mechanism for protocol-gating, so a muxed contract address cannot be constructed or processed by the host before the protocol version that includes this CAP.

Backwards Incompatibilities

This CAP does not introduce any backward incompatibilities. The SC_ADDRESS_TYPE_MUXED_CONTRACT variant is additive, and before the protocol upgrade it is not possible to upload Wasm that constructs or processes a muxed contract address. Downstream consumers that parse SCAddress will need to handle the new variant and the new strkey type.

Resource Utilization

The change has a negligible impact on resource utilization. A muxed contract address is 8 bytes larger than a non-muxed contract address, and the host operations on it lean on the existing metering primitives used for muxed account addresses.

Security Concerns

This CAP does not change the on-chain authorization or balance semantics of a transfer: a muxed contract address behaves identically to its underlying non-muxed contract address, with the multiplexing identifier serving only to convey off-chain routing information in the event payload. The storage-key prohibition prevents a muxed contract address from being inadvertently persisted as part of contract data, consistent with the treatment of muxed account addresses in CAP-67.

Test Cases

TBD

Implementation

TBD

Preamble

Status
Accepted
Protocol version
TBD
Authors
Jake Urban
Created
2026-06-24

Discussion

0 linked threads