Skip to main content

Token Interface

Token contracts, including the Stellar Asset Contract and example token implementations expose the following common interface.

Tokens deployed on Soroban can implement any interface they choose, however, they should satisfy the following interface to be interoperable with contracts built to support Soroban's built-in tokens.

Note, that in the specific cases the interface doesn't have to be fully implemented. For example, the custom token may not implement the administrative interface compatible with the Stellar Asset Contract - it won't stop it from being usable in the contracts that only perform the regular user operations (transfers, allowances, balances etc.).

Compatibility Requirements

For any given contract function, there are 3 requirements that should be consistent with the interface described here:

  • Function interface (name and arguments) - if not consistent, then the users simply won't be able to use the function at all. This is the hard requirement.
  • Authorization - the users have to authorize the token function calls with all the arguments of the invocation (see the interface comments). If this is inconsistent, then the custom token may have issues with getting the correct signatures from the users and may also confuse the wallet software.
  • Events - the token has to emit the events in the specified format. If inconsistent, then the token may not be handled correctly by the downstream systems such as block explorers.

Code

The interface below uses the Rust soroban-sdk to declare a trait that complies with the SEP-41 token interface.

pub trait TokenInterface {
/// Returns the allowance for `spender` to transfer from `from`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens to be drawn from.
/// * `spender` - The address spending the tokens held by `from`.
fn allowance(env: Env, from: Address, spender: Address) -> i128;

/// Set the allowance by `amount` for `spender` to transfer/burn from
/// `from`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens to be drawn from.
/// * `spender` - The address being authorized to spend the tokens held by
/// `from`.
/// * `amount` - The tokens to be made available to `spender`.
/// * `expiration_ledger` - The ledger number where this allowance expires. Cannot
/// be less than the current ledger number unless the amount is being set to 0.
/// An expired entry (where expiration_ledger < the current ledger number)
/// should be treated as a 0 amount allowance.
///
/// # Events
///
/// Emits an event with topics `["approve", from: Address,
/// spender: Address], data = [amount: i128, expiration_ledger: u32]`
fn approve(env: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32);

/// Returns the balance of `id`.
///
/// # Arguments
///
/// * `id` - The address for which a balance is being queried. If the
/// address has no existing balance, returns 0.
fn balance(env: Env, id: Address) -> i128;

/// Transfer `amount` from `from` to `to`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens which will be
/// withdrawn from.
/// * `to` - The address which will receive the transferred tokens.
/// * `amount` - The amount of tokens to be transferred.
///
/// # Events
///
/// Emits an event with topics `["transfer", from: Address, to: Address],
/// data = amount: i128`
fn transfer(env: Env, from: Address, to: Address, amount: i128);

/// Transfer `amount` from `from` to `to`, consuming the allowance of
/// `spender`. Authorized by spender (`spender.require_auth()`).
///
/// # Arguments
///
/// * `spender` - The address authorizing the transfer, and having its
/// allowance consumed during the transfer.
/// * `from` - The address holding the balance of tokens which will be
/// withdrawn from.
/// * `to` - The address which will receive the transferred tokens.
/// * `amount` - The amount of tokens to be transferred.
///
/// # Events
///
/// Emits an event with topics `["transfer", from: Address, to: Address],
/// data = amount: i128`
fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128);

/// Burn `amount` from `from`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens which will be
/// burned from.
/// * `amount` - The amount of tokens to be burned.
///
/// # Events
///
/// Emits an event with topics `["burn", from: Address], data = amount:
/// i128`
fn burn(env: Env, from: Address, amount: i128);

/// Burn `amount` from `from`, consuming the allowance of `spender`.
///
/// # Arguments
///
/// * `spender` - The address authorizing the burn, and having its allowance
/// consumed during the burn.
/// * `from` - The address holding the balance of tokens which will be
/// burned from.
/// * `amount` - The amount of tokens to be burned.
///
/// # Events
///
/// Emits an event with topics `["burn", from: Address], data = amount:
/// i128`
fn burn_from(env: Env, spender: Address, from: Address, amount: i128);

/// Returns the number of decimals used to represent amounts of this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn decimals(env: Env) -> u32;

/// Returns the name for this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn name(env: Env) -> String;

/// Returns the symbol for this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn symbol(env: Env) -> String;
}
CAUTION WHEN MODIFYING ALLOWANCES

The approve function overwrites the previous value with amount, so it is possible for the previous allowance to be spent in an earlier transaction before amount is written in a later transaction. The result of this is that spender can spend more than intended. This issue can be avoided by first setting the allowance to 0, verifying that the spender didn't spend any portion of the previous allowance, and then setting the allowance to the new desired amount. You can read more about this issue here - https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729.

Metadata

Another requirement for complying with the token interface is to write the standard metadata (decimal, name, and symbol) for the token in a specific format. This format allows users to directly read constant data from the ledger instead of invoking a Wasm function. The token example demonstrates how to use the Rust soroban-token-sdk to write the metadata, and we strongly encourage token implementations to follow this approach.

Handling Failure Conditions

In the token interface, there are several instances where function calls can fail due to various reasons such as lack of proper authorization, insufficient allowance or balance, etc. To handle these failure conditions, it is important to specify the expected behavior when such situations arise.

Its important to note the that the token interface not only incorporates the authorization concept for matching asset authorization in Stellar Classic, but it also utilizes the Soroban authorization mechanism. So, if you try to make a token call and it fails, it could be because of either token authorization processes.

To provide more context, when you use the token interface, there is a function called authorized that returns "true" if an address has token authorization.

More details on Authorization can be found here.

For the functions in the token interface, trapping should be used as the standard way to handle failure conditions since the interface is not designed to return error codes. This means that when a function encounters an error, it will halt execution and revert any state changes that occurred during the function call.

Failure Conditions

Here is a list of basic failure conditions and their expected behavior for functions in the token interface:

Admin functions:

  • If the admin did not authorize the call, the function should trap.
  • If the admin attempts to perform an invalid action (e.g., minting a negative amount), the function should trap.

Token functions:

  • If the caller is not authorized to perform the action (e.g., transferring tokens without proper authorization), the function should trap.
  • If the action would result in an invalid state (e.g., transferring more tokens than available in the balance or allowance), the function should trap.

Example: Handling Insufficient Allowance in burn_from function

In the burn_from function, the token contract should check whether the spender has enough allowance to burn the specified amount of tokens from the from address. If the allowance is insufficient, the function should trap, halting execution and reverting any state changes.

Here's an example of how the burn_from function can be modified to handle this failure condition:

fn burn_from(
env: soroban_sdk::Env,
spender: Address,
from: Address,
amount: i128,
) {
// Check if the spender has enough allowance
let current_allowance = allowance(env, from, spender);
if current_allowance < amount {
// Trap if the allowance is insufficient
panic!("Insufficient allowance");
}

// Proceed with burning tokens
// ...
}

By clearly outlining how to handle failures and incorporating the right error management techniques in the token interface, we can make token contracts stronger and safer.