Skip to main content

Token Contract

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

caution

The token contract is in early development, has not been audited, and is not intended for use. 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. The current iteration of the standard token contract doesn't interoperate with "classic" Stellar assets, but this feature will be available in the future. 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 authorization 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

Gettors 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: Signing payloads

The example from the test Token wrapper class demonstrate how you sign a payload for the token contract.

pub fn approve(&self, from: &Keypair, spender: &Identifier, amount: &BigInt) {
let from_id = to_ed25519(&self.env, from);
let nonce = self.nonce(&from_id);

let msg = SignaturePayload::V0(SignaturePayloadV0 {
function: symbol!("approve"),
contract: self.contract_id.clone(),
network: self.env.ledger().network_passphrase(),
args: (from_id, &nonce, spender, amount).into_val(&self.env),
});

let auth = Signature::Ed25519(Ed25519Signature {
public_key: from.public.to_bytes().into_val(&self.env),
signature: from.sign(msg).unwrap().into_val(&self.env),
});
TokenClient::new(&self.env, &self.contract_id).approve(&auth, &nonce, &spender, &amount)
}

Contract Interface

// Sets the administrator to "admin". Also sets some metadata
fn initialize(e: Env, admin: Identifier, decimal: u32, name: Bytes, symbol: Bytes);

// 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.