|
| 1 | +## Preamble |
| 2 | + |
| 3 | +``` |
| 4 | +CAP: 0084 |
| 5 | +Title: Muxed Contract Addresses |
| 6 | +Working Group: |
| 7 | + Owner: Jake Urban <jake@stellar.org> |
| 8 | + Authors: Jake Urban <jake@stellar.org> |
| 9 | + Consulted: Dmytro Kozhevin <@dmkozh>, Leigh McCulloch <@leighmcculloch> |
| 10 | +Status: Draft |
| 11 | +Created: 2026-06-24 |
| 12 | +Discussion: https://github.com/stellar/stellar-protocol/discussions/1950 |
| 13 | +Protocol version: 28 |
| 14 | +``` |
| 15 | + |
| 16 | +## Simple Summary |
| 17 | + |
| 18 | +Extend the address multiplexing introduced for Stellar accounts in [CAP-67](cap-0067.md) 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. |
| 19 | + |
| 20 | +## Working Group |
| 21 | + |
| 22 | +As described in the preamble section. |
| 23 | + |
| 24 | +## Motivation |
| 25 | + |
| 26 | +[CAP-67](cap-0067.md) 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. |
| 27 | + |
| 28 | +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. |
| 29 | + |
| 30 | +This CAP removes that asymmetry by replicating the CAP-67 account multiplexing design for contract addresses. |
| 31 | + |
| 32 | +### Goals Alignment |
| 33 | + |
| 34 | +This CAP is aligned with the following Stellar Network Goals: |
| 35 | + |
| 36 | + - The Stellar Network should make it easy for developers of Stellar projects to create highly usable products. |
| 37 | + |
| 38 | +## Abstract |
| 39 | + |
| 40 | +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` and `mint` functions accept a muxed contract address as the destination and emit 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. |
| 41 | + |
| 42 | +## Specification |
| 43 | + |
| 44 | +### XDR changes |
| 45 | + |
| 46 | +This patch of XDR changes is based on the latest protocol 27 XDR files in [stellar-xdr](https://github.com/stellar/stellar-xdr). The exact base commit will be pinned when the XDR is agreed on in a protocol meeting. |
| 47 | + |
| 48 | +```diff mddiffcheck.ignore=true |
| 49 | +diff --git a/Stellar-contract.x b/Stellar-contract.x |
| 50 | +--- a/Stellar-contract.x |
| 51 | ++++ b/Stellar-contract.x |
| 52 | +@@ |
| 53 | + enum SCAddressType |
| 54 | + { |
| 55 | + SC_ADDRESS_TYPE_ACCOUNT = 0, |
| 56 | + SC_ADDRESS_TYPE_CONTRACT = 1, |
| 57 | + SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2, |
| 58 | + SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3, |
| 59 | +- SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4 |
| 60 | ++ SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4, |
| 61 | ++ SC_ADDRESS_TYPE_MUXED_CONTRACT = 5 |
| 62 | + }; |
| 63 | + |
| 64 | + struct MuxedEd25519Account |
| 65 | + { |
| 66 | + uint64 id; |
| 67 | + uint256 ed25519; |
| 68 | + }; |
| 69 | + |
| 70 | ++struct MuxedContract |
| 71 | ++{ |
| 72 | ++ uint64 id; |
| 73 | ++ ContractID contractId; |
| 74 | ++}; |
| 75 | ++ |
| 76 | + union SCAddress switch (SCAddressType type) |
| 77 | + { |
| 78 | + case SC_ADDRESS_TYPE_ACCOUNT: |
| 79 | + AccountID accountId; |
| 80 | + case SC_ADDRESS_TYPE_CONTRACT: |
| 81 | + ContractID contractId; |
| 82 | + case SC_ADDRESS_TYPE_MUXED_ACCOUNT: |
| 83 | + MuxedEd25519Account muxedAccount; |
| 84 | + case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE: |
| 85 | + ClaimableBalanceID claimableBalanceId; |
| 86 | + case SC_ADDRESS_TYPE_LIQUIDITY_POOL: |
| 87 | + PoolID liquidityPoolId; |
| 88 | ++case SC_ADDRESS_TYPE_MUXED_CONTRACT: |
| 89 | ++ MuxedContract muxedContract; |
| 90 | + }; |
| 91 | +``` |
| 92 | + |
| 93 | +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. |
| 94 | + |
| 95 | +### Semantics |
| 96 | + |
| 97 | +#### `MuxedAddressObject` host object |
| 98 | + |
| 99 | +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`: |
| 100 | + |
| 101 | +- (unchanged) `SC_ADDRESS_TYPE_ACCOUNT` and `SC_ADDRESS_TYPE_CONTRACT` correspond to `AddressObject` |
| 102 | +- (unchanged) `SC_ADDRESS_TYPE_MUXED_ACCOUNT` corresponds to `MuxedAddressObject` |
| 103 | +- `SC_ADDRESS_TYPE_MUXED_CONTRACT` corresponds to `MuxedAddressObject` |
| 104 | +- (unchanged) `SC_ADDRESS_TYPE_CLAIMABLE_BALANCE` and `SC_ADDRESS_TYPE_LIQUIDITY_POOL` are disallowed by the host |
| 105 | + |
| 106 | +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. |
| 107 | + |
| 108 | +The two host functions introduced in CAP-67 operate on a muxed contract address as follows: |
| 109 | + |
| 110 | +- `get_address_from_muxed_address` returns the contract address part as an `AddressObject` of type `SC_ADDRESS_TYPE_CONTRACT`, stripping the multiplexing identifier. |
| 111 | +- `get_id_from_muxed_address` returns the multiplexing identifier as a `U64Val`. |
| 112 | + |
| 113 | +#### `SC_ADDRESS_TYPE_MUXED_CONTRACT` is prohibited in storage keys |
| 114 | + |
| 115 | +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. |
| 116 | + |
| 117 | +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. |
| 118 | + |
| 119 | +#### Update the SAC `transfer` and `mint` functions to support muxed contract addresses |
| 120 | + |
| 121 | +The `transfer` and `mint` functions of the Stellar Asset Contract already accept an `AddressObject` or `MuxedAddressObject` for the `to` argument (see CAP-67). This CAP requires no change to the function signatures: 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. |
| 122 | + |
| 123 | +#### Events |
| 124 | + |
| 125 | +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`. |
| 126 | + |
| 127 | +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`. |
| 128 | + |
| 129 | +A muxed contract destination can only originate from an invocation of the Stellar Asset Contract `transfer` or `mint` function, so 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. |
| 130 | + |
| 131 | +#### Strkey encoding |
| 132 | + |
| 133 | +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. |
| 134 | + |
| 135 | +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). |
| 136 | + |
| 137 | +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. |
| 138 | + |
| 139 | +## Design Rationale |
| 140 | + |
| 141 | +### Parity with CAP-67 |
| 142 | + |
| 143 | +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. |
| 144 | + |
| 145 | +### Reusing `MuxedAddressObject` rather than adding a new host object |
| 146 | + |
| 147 | +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. |
| 148 | + |
| 149 | +### Destination-only support |
| 150 | + |
| 151 | +This CAP only enables a muxed contract address to be used as the destination of a `transfer` or `mint`, 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. |
| 152 | + |
| 153 | +### Limiting scope to `transfer` and `mint` |
| 154 | + |
| 155 | +The Stellar Asset Contract `transfer` and `mint` functions are the same functions that CAP-67 updated to accept a muxed account destination. Extending support to additional functions that take a destination argument, such as `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. |
| 156 | + |
| 157 | +### Strkey defined in SEP-23 |
| 158 | + |
| 159 | +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. |
| 160 | + |
| 161 | +## Protocol Upgrade Transition |
| 162 | + |
| 163 | +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` and `mint` functions begin 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. |
| 164 | + |
| 165 | +### Backwards Incompatibilities |
| 166 | + |
| 167 | +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. |
| 168 | + |
| 169 | +### Resource Utilization |
| 170 | + |
| 171 | +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. |
| 172 | + |
| 173 | +## Security Concerns |
| 174 | + |
| 175 | +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. |
| 176 | + |
| 177 | +## Test Cases |
| 178 | + |
| 179 | +TBD |
| 180 | + |
| 181 | +## Implementation |
| 182 | + |
| 183 | +TBD |
0 commit comments