Skip to main content

Token Contract

The token contract is an implementation of CAP-46-6 Smart Contract Standardized Asset.

caution

The token contract is in early development, has not been audited, and is intended for use in development and testing only at this stage.. Report issues here.

Overview

Tokens are a vital part of blockchains, and contracts that implement token functionality are inevitable on any smart contract platform. A built-in token contract has a number of advantages over token contracts written by the ecosystem. First, we can special case this contract and run it natively instead of running in a WASM VM, reducing the cost of using the contract. Second, we can use this built-in contract to allow "classic" Stellar assets to interoperate with Soroban. Note that this standard token contract does not prevent the ecosystem from developing other token contracts if the standard is missing functionality they require.

The standard token contract is similar to the widely used ERC-20 token standard, which should make it easier for existing smart contract developers to get started on Stellar.

Token contract authorization semantics

See the advanced auth example for an overview of authorization.

Token operations

The token contract contains three kinds of operations

  • getters, such as balance, which do not change the state of the contract
  • unprivileged mutators, such as approve and xfer, which change the state of the contract but do not require special privileges
  • privileged mutators, such as burn and set_admin, which change the state of the contract but require special privileges

Getters require no authorization because they do not change the state of the contract and all contract data is public. For example, balance simply returns the balance of the specified identity without changing it.

Unprivileged mutators require authorization from some identity. The identity which must provide authorization will vary depending on the unprivileged mutator. For example, a "grantor" can use approve to allow a "spender" to spend the grantor's money up to some limit. So for approve, the grantor must provide authorization. Similarly, a "sender" can use xfer to send money to a "recipient". So for xfer, the sender must provide authorization.

Priviliged mutators require authorization from a specific privileged identity, known as the "administrator". For example, only the administrator can mint more of the asset. Similarly, only the administrator can appoint a new administrator.

Replay prevention

The token contract provides replay prevention by using a nonce. The messages that are signed to provide authorization contain a nonce. The contract also stores a nonce per identity. When checking signatures, the contract loads the nonce for the relevant identity. When an operation succeeds, the nonce stored in the contract is incremented. This makes it impossible to reuse a signature.

The current nonce for an identity can be retrieved using the nonce contract function.

Example: Invoker auth

The easiest way to use the built-in token with classic accounts is to just use the invoker auth. In this way there won't be a need to sign the contract payload, so the contract call would look like this:

// `with_source_account` is a testing utility, but the contract call arguments
// would be the same for the real contract call too.
token.with_source_account(&token_admin_id).mint(
// No signature needed, just a flag that invoker auth should be used, i.e.
// `token_admin_id` in this case.
&Signature::Invoker,
// Nonce is always 0 for invokers.
&BigInt::zero(&env),
&user_id,
&BigInt::from_u32(&env, 1000),
);

See a more complete example that uses invoker auth in the tests here.

Example: Signing payloads

The payload signature semantics is the same as for regular contracts using advanced auth. The following snippet shows how should the signature payload look like:

let nonce = token.nonce(&token_admin_id);
// This is the test call, but the contract call arguments and signature payload
// would be the same for the real contract call too.
let sig = soroban_auth::testutils::ed25519::sign(
&env,
// Signer has the private key of the admin.
&token_admin_signer,
// Identifier of the token contract.
&token_contract_id,
// Name of the contract function we call.
symbol!("mint"),
// Arguments of the contract function call.
// Notice that instead of the signature (first `mint` argument), public key
// is used as the first argument here.
(&token_admin_id, &nonce, &user_id, &BigInt::from_u32(&env, 1000)),
);
// Call the contract with signature we computed above.
token.mint(
&sig,
&nonce,
&user_id,
&BigInt::from_u32(&env, 1000),
);

Interacting with classic Stellar assets

Token contract is the only way to interact with 'classic' Stellar assets in Soroban. 'Classic' assets include native Stellar token (lumens) and all the existing trustlines.

For every 'classic' asset exactly one respective token contract can be deployed via create_token_from_asset host function. The resulting token will have a deterministic identifier. The issuer of the asset will be the administrator of the deployed contract. Native Stellar token doesn't have an administrator.

After the contract has been deployed the users can use import function to move part of their existing balance to the contract or export to move the token balance back to their 'classic' balance. Otherwise, the token will behave in exactly the same way as any other token, i.e. users that don't have a corresponding trustline or even a Stellar account can still use it (with the exception of the import/export functions).

Contract Interface

This interface can be found in the SDK. It extends the common token interface with import/export functions for classic asset interactions.

// The metadata used to initialize token (doesn't apply to contracts representing 
// 'classic' Stellar assets).
pub struct TokenMetadata {
pub name: Bytes,
pub symbol: Bytes,
pub decimals: u32,
}

// Initializes a 'smart-only' token by setting its admin and metadata.
// Tokens that represent 'classic' Stellar assets don't need to call this, as
// their metadata is inherited from the existing assets.
fn init(env: Env, admin: Identifier, metadata: TokenMetadata);

// Functions that apply on for tokens representing classic assets.

// Moves the `amount` from classic asset balance to the token balance of `id`
// user.
// `id` must be a classic Stellar account (i.e. an account invoker or signature
// signed by an account).
fn import(env: Env, id: Signature, nonce: BigInt, amount: i64);

// Moves the `amount` from token balance to the classic asset balance of `id`
// user.
// `id` must be a classic Stellar account (i.e. an account invoker or signature
// signed by an account).
fn export(env: Env, id: Signature, nonce: BigInt, amount: i64);

// Admin interface -- these functions are privileged

// If "admin" is the administrator, burn "amount" from "from"
fn burn(e: Env, admin: Signature, nonce: BigInt, from: Identifier, amount: BigInt);

// If "admin" is the administrator, mint "amount" to "to"
fn mint(e: Env, admin: Signature, nonce: BigInt, to: Identifier, amount: BigInt);

// If "admin" is the administrator, set the administrator to "id"
fn set_admin(e: Env, admin: Signature, nonce: BigInt, new_admin: Identifier);

// If "admin" is the administrator, freeze "id"
fn freeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier);

// If "admin" is the administrator, unfreeze "id"
fn unfreeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier);

// Token Interface

// Get the allowance for "spender" to transfer from "from"
fn allowance(e: Env, from: Identifier, spender: Identifier) -> BigInt;

// Set the allowance to "amount" for "spender" to transfer from "from"
fn approve(e: Env, from: Signature, nonce: BigInt, spender: Identifier, amount: BigInt);

// Get the balance of "id"
fn balance(e: Env, id: Identifier) -> BigInt;

// Transfer "amount" from "from" to "to"
fn xfer(e: Env, from: Signature, nonce: BigInt, to: Identifier, amount: BigInt);

// Transfer "amount" from "from" to "to", consuming the allowance of "spender"
fn xfer_from(
e: Env,
spender: Signature,
nonce: BigInt,
from: Identifier,
to: Identifier,
amount: BigInt,
);

// Returns true if "id" is frozen
fn is_frozen(e: Env, id: Identifier) -> bool;

// Returns the current nonce for "id"
fn nonce(e: Env, id: Identifier) -> BigInt;

// Descriptive Interface

// Get the number of decimals used to represent amounts of this token
fn decimals(e: Env) -> u32;

// Get the name for this token
fn name(e: Env) -> Bytes;

// Get the symbol for this token
fn symbol(e: Env) -> Bytes;

Interacting with the token contract in tests

See interacting with contracts in tests for more general information on this topic.