Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Token interface and single balance update #257

Merged
merged 2 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 66 additions & 130 deletions docs/built-in-contracts/token.mdx
Original file line number Diff line number Diff line change
@@ -1,49 +1,91 @@
---
sidebar_position: 1
title: Token Contract
title: Stellar Asset Contract
---

# Token Contract
# Stellar Asset Contract

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

[CAP-46-6 Smart Contract Standardized Asset]: https://stellar.org/protocol/cap-46-06

:::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
intended for use in development and testing only at this stage. Report issues
[here](https://github.com/stellar/soroban-token-contract/issues/new/choose).
:::

## 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.
Stellar has numerous assets on its classic network, and being able to use them
in Soroban would give users much more flexibility with how they can use their
assets. For this reason, we introduced the Stellar Asset Contract, or SAC for
short, which will allow users to use their Stellar account and trustline
balances in Soroban.

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.
The SAC implements the [token interface](../common-interfaces/token.mdx), which is
similar to the widely used ERC-20 token standard. This should make it easier for
existing smart contract developers to get started on Stellar.

## Token contract authorization semantics
## Deployment

For every 'classic' asset exactly one respective Stellar Asset Contract can be
deployed. It can be deployed using the `InvokeHostFunctionOp` with
`HOST_FUNCTION_TYPE_CREATE_CONTRACT` and `CONTRACT_ID_FROM_ASSET` specified
[here](../tutorials/invoking-contracts-with-transactions). The resulting token
will have a deterministic identifier, which will be the sha256 hash of
`HashIDPreimage::ENVELOPE_TYPE_CONTRACT_ID_FROM_ASSET` xdr specified
[here](https://github.com/stellar/stellar-xdr/blob/026c9cd074bdb28ddde8ee52f2a4502d9e518a09/Stellar-transaction.x#L637).

## Interacting with classic Stellar assets

The Stellar Asset 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. The issuer of the asset will be the
administrator of the deployed contract. Because the 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 or
trustline balance. There are some differences depending on if you are using a
classic account identifier vs a non-account identifier like a contract.

- Using `Identifier::Account`
- The balance must exist in a trustline or an account. This means the contract
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean trustline of an account?
Or do you mean trustline or an account (in the case of Lumen)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Account in the case of lumens. I'll update this to make it clear.

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.
- Classic trustline semantics will be followed.
- Transfers will only succeed if the corresponding
trustline(s) have the `AUTHORIZED_FLAG` set.
- A trustline balance can only be clawed back using the `clawback`
contract function if the trustline has `TRUSTLINE_CLAWBACK_ENABLED_FLAG`
set.
- The admin can only deauthorize a trustline if the issuer of the asset has
`AUTH_REVOCABLE` set. The deauthorization will fail if the issuer is
missing. 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.
- 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.
- Using `Identifier::Ed25519` or `Identifier::Contract`
- The balance and authorization state will be stored in ContractData, as
opposed to a trustline.
- These balances are stored in a 128-bit signed integer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth mentioning how the 128-bit balance interops with the classic 64-bit balance?


## Authorization semantics

See the [advanced auth example](../examples/auth-advanced) for an overview of
authorization.

### Token operations
### SAC 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
- unprivileged mutators, such as `incr_allow` 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
- privileged mutators, such as `clawback` 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
Expand All @@ -52,8 +94,8 @@ 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
mutator. For example, a "grantor" can use `incr_allow` to allow a "spender" to spend
the grantor's money up to some limit. So for `incr_allow`, 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.

Expand Down Expand Up @@ -128,116 +170,10 @@ token.mint(
);
```

## 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](https://github.com/stellar/rs-soroban-sdk/blob/47895a87b98a2fb8dd882839df7abde03f6c7fc6/soroban-token-spec/src/lib.rs#L27). It
extends the common [token interface](../common-interfaces/token.mdx) with
`import`/`export` functions for classic asset interactions.

```rust
// 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: i128, 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: i128, amount: i64);

// Admin interface -- these functions are privileged

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

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

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

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

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

// Token Interface

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

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

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

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

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

// 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) -> i128;

// 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;
```
This interface can be found in the [SDK](https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-token-spec/src/lib.rs). It
extends the common [token interface](../common-interfaces/token.mdx).

## Interacting with the token contract in tests

Expand Down
85 changes: 50 additions & 35 deletions docs/common-interfaces/token.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ sidebar_position: 1
title: Token Interface
---

Token contracts, including the built-in token and example token implementations
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,
Expand All @@ -12,16 +12,13 @@ built to support Soroban's built-in tokens.

```rust
pub trait Contract {
/// Sets the administrator to "admin" and metadata.
fn init(env: soroban_sdk::Env, admin: soroban_auth::Identifier, metadata: TokenMetadata);

// --------------------------------------------------------------------------------
// Admin interface – privileged functions.
// --------------------------------------------------------------------------------

/// If "admin" is the administrator, burn "amount" from "from".
/// Emit event with topics = ["burn", from: Identifier, to: Identifier], data = [amount: i128]
fn burn(
/// If "admin" is the administrator, clawback "amount" from "from". "amount" is burned.
/// Emit event with topics = ["clawback", from: Identifier, to: Identifier], data = [amount: i128]
fn clawback(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably good to specify some minimal set of failure conditions. E.g. if the "admin" is not the administrator, and its expected behavior (returning error code or trapping).
This applies to most of the interface functions below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue for this

env: soroban_sdk::Env,
admin: soroban_auth::Signature,
nonce: i128,
Expand All @@ -48,22 +45,15 @@ pub trait Contract {
new_admin: soroban_auth::Identifier,
);

/// If "admin" is the administrator, freeze "id".
/// Emit event with topics = ["freeze", admin: Identifier], data = [id: Identifier]
fn freeze(
env: soroban_sdk::Env,
admin: soroban_auth::Signature,
nonce: i128,
id: soroban_auth::Identifier,
);

/// If "admin" is the administrator, unfreeze "id".
/// Emit event with topics = ["unfreeze", admin: Identifier], data = [id: Identifier]
fn unfreeze(
/// If "admin" is the administrator, set the authorize state of "id" to "authorize".
/// If "authorize" is true, "id" should be able to use its balance.
/// Emit event with topics = ["set_auth", admin: Identifier, id: Identifier], data = [authorize: bool]
fn set_auth(
env: soroban_sdk::Env,
admin: soroban_auth::Signature,
nonce: i128,
id: soroban_auth::Identifier,
authorize: bool,
);

// --------------------------------------------------------------------------------
Expand All @@ -77,9 +67,20 @@ pub trait Contract {
spender: soroban_auth::Identifier,
) -> i128;

/// Set the allowance to "amount" for "spender" to transfer from "from".
/// Emit event with topics = ["approve", from: Identifier, spender: Identifier], data = [amount: i128]
fn approve(
/// Increase the allowance by "amount" for "spender" to transfer/burn from "from".
/// Emit event with topics = ["incr_allow", from: Identifier, spender: Identifier], data = [amount: i128]
fn incr_allow(
env: soroban_sdk::Env,
from: soroban_auth::Signature,
nonce: i128,
spender: soroban_auth::Identifier,
amount: i128,
);

/// Decrease the allowance by "amount" for "spender" to transfer/burn from "from".
/// If "amount" is greater than the current allowance, set the allowance to 0.
/// Emit event with topics = ["decr_allow", from: Identifier, spender: Identifier], data = [amount: i128]
fn decr_allow(
env: soroban_sdk::Env,
from: soroban_auth::Signature,
nonce: i128,
Expand All @@ -90,6 +91,11 @@ pub trait Contract {
/// Get the balance of "id".
fn balance(env: soroban_sdk::Env, id: soroban_auth::Identifier) -> i128;

/// Get the spendable balance of "id". This will return the same value as balance()
/// unless this is called on the Stellar Asset Contract, in which case this can
/// be less due to reserves/liabilities.
fn spendable(env: soroban_sdk::Env, id: soroban_auth::Identifier) -> i128;

/// Transfer "amount" from "from" to "to.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention in the comment of this function "Transfers to the issuer account will burn the token, while transfers from the issuer account will mint." (what you had in the explanation)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issuer is an implementation detail of the SAC, and will not apply to any other token contracts, so I don't think that belongs here.

/// Emit event with topics = ["transfer", from: Identifier, to: Identifier], data = [amount: i128]
fn xfer(
Expand All @@ -111,8 +117,27 @@ pub trait Contract {
amount: i128,
);

// Returns true if "id" is frozen.
fn is_frozen(env: soroban_sdk::Env, id: soroban_auth::Identifier) -> bool;
/// Burn "amount" from "from".
/// Emit event with topics = ["burn", from: Identifier], data = [amount: i128]
fn burn(
env: soroban_sdk::Env,
from: soroban_auth::Signature,
nonce: i128,
amount: i128,
);

/// Burn "amount" from "from", consuming the allowance of "spender".
/// Emit event with topics = ["burn", from: Identifier], data = [amount: i128]
fn burn_from(
env: soroban_sdk::Env,
spender: soroban_auth::Signature,
nonce: i128,
from: soroban_auth::Identifier,
amount: i128,
);

// Returns true if "id" is authorized to use its balance.
fn authorized(env: soroban_sdk::Env, id: soroban_auth::Identifier) -> bool;

// Returns the current nonce for "id".
fn nonce(env: soroban_sdk::Env, id: soroban_auth::Identifier) -> i128;
Expand All @@ -133,17 +158,7 @@ pub trait Contract {
```

Most types that a token implementation utilizes are provided by the
[soroban-sdk] and [soroban-auth] crates. The following type is also used during
initialization.

```rust
#[soroban_sdk::contracttype]
pub struct TokenMetadata {
pub name: soroban_sdk::Bytes,
pub symbol: soroban_sdk::Bytes,
pub decimals: u32,
}
```
[soroban-sdk] and [soroban-auth] crates.

[soroban-sdk]: ../SDKs/rust
[soroban-auth]: ../SDKs/rust-auth