The term “custom token” has been deprecated in favor of “contract token”. View the conversation in the Stellar Developer Discord.
Stellar Asset Contract (SAC)
The Stellar Asset Contract (SAC) is an implementation of CAP-46-6 Smart Contract Standardized Asset and SEP-41 Token Interface for Stellar assets. See examples of how to use the SAC in the Tokens How-To Guides.Overview
Stellar assets are issued by Stellar accounts. Issue an asset on Stellar by following the Issue an Asset Tutorial.
Deployment
Every Stellar asset on Stellar has reserved a contract address that the Stellar Asset Contract can be deployed to. Anyone can initiate the deploy and the Stellar asset issuer does not need to be involved. It can be deployed using the Stellar CLI as shown here. Or the Stellar SDK can be used as shown here by callingInvokeHostFunctionOp with HOST_FUNCTION_TYPE_CREATE_CONTRACT and a CONTRACT_ID_PREIMAGE_FROM_ASSET contract ID preimage. The resulting token will have a deterministic identifier, which will be the sha256 hash of HashIDPreimage::ENVELOPE_TYPE_CONTRACT_ID xdr specified here.
Anyone can deploy the instances of Stellar Asset Contract. Note, that the initialization of the Stellar Asset Contracts happens automatically during the deployment. The asset issuer becomes the initial administrator once the contract has been deployed; administrative authority is mutable and can be transferred afterwards with the SAC’s set_admin function.
A deployed SAC can be identified on-chain from its contract instance: its SCContractInstance.executable is CONTRACT_EXECUTABLE_STELLAR_ASSET rather than a Wasm hash. Once a contract is verified as a SAC this way, its built-in name() and symbol() methods report the wrapped asset’s identity — there is no generic SEP-41 asset()/issuer() accessor to rely on. Note that a contract-address hash and the current admin value are not, by themselves, proof of an asset’s provenance: verify the executable first, since the administrator may have been changed via set_admin.
Interacting with classic Stellar assets
The Stellar Asset Contract is the only way for contracts to interact with Stellar assets, either the native XLM asset, or those issued by Stellar accounts. The issuer of the asset will be the initial administrator of the deployed contract (administrative authority can later be moved withset_admin). Because the Native Stellar token doesn’t have an issuer, it will not have an administrator either. It also cannot be burned.
After the contract has been deployed, users can use their classic account (for lumens) or trustline (for other assets) balance. There are some differences depending on if you are using a classic account Address vs a contract Address (corresponding either to a regular contract or to a custom account contract). The following section references some issuer and trustline flags from Stellar classic, which you can learn more about here.
- Using
Address::Account- The balance must exist in a trustline (or an account for the native balance). This means the contract will not store the balance in ContractData. If the trustline or account is missing, any function that tries to interact with that balance will fail (as of Yardstick, Protocol 26, a contract can create the missing trustline itself by first calling the SAC’s
trustfunction). - Classic trustline semantics will be followed.
- Transfers will only succeed if the corresponding trustline(s) have the
AUTHORIZED_FLAGset. - A trustline balance can only be clawed back using the
clawbackcontract function if the trustline hasTRUSTLINE_CLAWBACK_ENABLED_FLAGset. - Transfers to the issuer account will burn the token, while transfers from the issuer account will mint.
- Trustline balances are stored in a 64-bit signed integer even though the interface accepts 128-bit signed integers. Any operation that attempts to send or receive an amount more than the maximum amount that can be represented by a 64-bit signed integer will fail.
- Transfers will only succeed if the corresponding trustline(s) have the
- The balance must exist in a trustline (or an account for the native balance). This means the contract will not store the balance in ContractData. If the trustline or account is missing, any function that tries to interact with that balance will fail (as of Yardstick, Protocol 26, a contract can create the missing trustline itself by first calling the SAC’s
- Using
Address::Contract- The balance and authorization state will be stored in contract storage, as opposed to a trustline.
- Balances are stored in a 128-bit signed integer.
- A balance can only be clawed back if the issuer account had the
AUTH_CLAWBACK_ENABLED_FLAGset when the balance was created. A balance is created when either anAddress::Contractis on the receiving end of a successful transfer, or if the admin sets the authorization state. Read more aboutAUTH_CLAWBACK_ENABLED_FLAGhere.
Balance Authorization Required
In theAddress::Contract case, if the issuer has AUTH_REQUIRED_FLAG set, then the specified Address::Contract will need to be explicitly authorized with set_auth before it can receive a balance. This logic lines up with how trustlines interact with the AUTH_REQUIRED_FLAG issuer flag, allowing asset issuers to have the same control in Soroban as they do in Stellar classic. Read more about AUTH_REQUIRED_FLAG here.
Revoking Authorization
The admin can only revoke authorization from anAddress, if the issuer of the asset has AUTH_REVOCABLE_FLAG set. The deauthorization will fail if the issuer is missing. This requirement is true for both the trustline balances of Address::Account and contract balances of Address:Contract. Note that when a trustline is deauthorized from Soroban, AUTHORIZED_FLAG is cleared and AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG is set to avoid having to pull offers and redeeming pool shares.
Creating trustlines from a contract
As of Yardstick, Protocol 26 (CAP-73), the SAC’strust function allows a contract to create an asset’s trustline for a G... address as part of a contract invocation. Before Yardstick, Protocol 26, a missing trustline could only be created with a separate changeTrust operation, so any contract function touching a trustline balance would fail until the account holder set up the trustline in a separate transaction.
The trust function is useful any time a contract distributes an asset to accounts that may not hold it yet: for example, an airdrop or payout contract calling trust before mint or transfer, or bridging and chain abstraction flows that deliver assets to freshly created accounts. See setting a custom SAC admin for a worked example of a contract calling trust before minting to a recipient.
A few things to keep in mind:
- The call is a no-op if
addris a contract address, or if the trustline already exists, so it’s safe to call unconditionally before a transfer or mint. - When a trustline is actually created, the SAC requires authorization from
addr, preserving the opt-in nature of trustlines. - The account holding the new trustline must satisfy the network’s base reserve requirement for the additional ledger entry.
Sending to a muxed address
As of Whisk, Protocol 23 (CAP-67), theto argument of the SAC’s transfer function is a MuxedAddress rather than an Address. A MuxedAddress is either a plain address or an M... muxed account address, which pairs a G... account with a 64-bit multiplexing id. This is how a contract payment identifies one user of a pooled account, the way a memo does for a classic payment: a transaction that invokes a contract must set MEMO_NONE, so before Whisk there was no way to tag a SAC payment at all.
transfer is the only SAC function that accepts a MuxedAddress. approve, balance, transfer_from, burn, burn_from, mint, clawback, set_authorized and trust all still take an Address.
The multiplexing id changes nothing on the ledger. The SAC strips the id off the address, moves the balance exactly as it would for the underlying address, and reports the id in the event instead:
- The trustline rules for
Address::Accountare unchanged: the underlyingG...account still needs an authorized trustline for the asset, or the transfer fails. - The
totopic of the emitted event is the underlying address, never the muxed address. The id travels in the eventdatainstead. Only a call that passes a muxed address makesdataa map,{ amount: i128, to_muxed_id: u64 }; a call that passes a plain address keeps the plainamount: i128. - When one side of the transfer is the asset issuer, the SAC emits
mintorburnrather thantransfer. Themintevent keeps the id; theburnevent drops it. See Events: SEP-41 Tokens vs. Stellar Asset Contracts.
- The Soroban host rejects a muxed address in a contract data key, including instance storage. A contract that must keep both parts stores the address and the id as separate values.
- A contract function that declares an
Addressargument fails if the caller passes a muxed address. The reverse is safe: a function that declares aMuxedAddressstill accepts a plainAddress, so a contract token that moves itstransferargument toMuxedAddressdoes not break its existing callers. - Only Stellar accounts can be multiplexed. Muxed contract addresses do not exist in the current protocol.
MuxedAddress::address() returns the address part, and MuxedAddress::id() returns Some(id) for a muxed address and None for a plain one. On the client side, the JavaScript SDK builds the argument from the strkey with new Address('M...').toScVal().
See Send to and receive payments from Contract Accounts for a worked flow, including how the muxed id appears in Horizon and RPC responses.
Authorization semantics
See the authorization overview and auth example for general information about authorization in Soroban. The token contract contains three kinds of operations that follow the token interface:- getters, such as
balance, which do not change the state of the contract - unprivileged mutators, such as
incr_allowandxfer, which change the state of the contract but do not require special privileges - privileged mutators, such as
clawbackandset_admin, which change the state of the contract but require special privileges
balance simply returns the balance of the specified Address without changing it.
Unprivileged mutators require authorization from the Address that spends or allows spending their balance. The exceptions are xfer_from and burn_from operations where the Address that require authorization from the ‘spender’ entity that has got an allowance from another Address beforehand.
Priviliged mutators require authorization from a specific privileged identity, known as the “administrator”. For example, only the administrator can mint more of the token. Similarly, only the administrator can appoint a new administrator.
Contract Interface
Thetoken module of the Rust SDK contains two traits, and corresponding client structs, for interacting with SACs.
- The
TokenInterfacetrait andTokenClientstruct can be used to invoke the “basic subset” of the SAC functionality. They implement the common SEP-41 Token Interface, and include functions liketransfer,burn,allowance, etc. - The
StellarAssetInterfacetrait andStellarAssetClientstruct can be used to invoke the “extended subset” of SAC functionality. They extend the common SEP-41 token interface with administrative functionality. In addition to all theTokenInterfacefunctions, these traits contain admin functions likeset_admin,clawback,set_authorized, etc.