Skip to content

Latest commit

 

History

History
516 lines (426 loc) · 18.5 KB

cap-0046-06.md

File metadata and controls

516 lines (426 loc) · 18.5 KB
CAP: 0046-06 (formerly 0054)
Title: Smart Contract Standardized Asset (Stellar Asset Contract)
Working Group:
    Owner: Jonathan Jove <@jonjove>
    Authors: Jonathan Jove <@jonjove>, Siddharth Suresh <@sisuresh>,
    Consulted: Nicolas Barry <@monsieurnicolas>, Leigh McCulloch <@leighmcculloch>, Tomer Weller <@tomerweller>
Status: Final
Created: 2022-05-31
Discussion:
Protocol version: 20

Simple Summary

Allow contracts to interoperate with Stellar assets.

Motivation

A fungible asset is a fundamental concept on blockchains. Fungible assets can conceptually be divided into two pieces: standard functionality enabling push and pull transfers, and varying functionality around asset administration. Most blockchain ecosystems have very little innovation in the space of fungible assets, with developers often relying on open source implementations such as OpenZeppelin.

Rather than rely on an open source implementation, developers should have access to a native contract which fulfils typical needs. This does not prevent developers from implementing their own fungible asset if the contract does not meet their needs. But the efficiency gained from a native implementation should reduce fees sufficiently to encourage most developers to choose the native implementation when it is suitable.

The native implementation should serve as a compatibility layer for classic assets. This ensures that any contract which can interoperate with new smart assets can also interoperate with classic assets, and vice versa.

The advantage of a native implementation is that the semantics are known to the protocol. This allows additional enhancements like a dedicated fee lane for simple asset transfers, or the potential to use the asset in a high-throughput parallel exchange like SPEEDEX.

Goals Alignment

This CAP is aligned with the following Stellar Network Goals:

  • The Stellar Network should make it easy for developers of Stellar projects to create highly usable products
  • The Stellar Network should enable cross-border payments, i.e. payments via exchange of assets, throughout the globe, enabling users to make payments between assets in a manner that is fast, cheap, and highly usable.

Abstract

This proposal introduces a native contract implementation for classic tokens. The interface tries to follow an ERC-20 model.

Specification

XDR Changes

See the XDR diffs in the Soroban overview CAP, specifically those covering new envelope types.

Semantics: Data Format

The following types are used throughtout this proposal.

/******************************************************************************\
*
* Data Structures
*
\******************************************************************************/

pub struct AllowanceDataKey {
    pub from: Address,
    pub spender: Address,
}

pub struct AllowanceValue {
    pub amount: i128,
    pub expiration_ledger: u32,
}

pub struct BalanceValue {
    pub amount: i128,
    pub authorized: bool,
    pub clawback: bool,
}

/// Keys for the persistent data associated with token users.
pub enum DataKey {
    Allowance(AllowanceDataKey),
    Balance(Address),
}

/// Keys for token instance data.
pub enum InstanceDataKey {
    Admin,
    AssetInfo,
}

Semantics: Initialization

/******************************************************************************\
*
* Initialization
*
\******************************************************************************/

/// init_asset can create a contract that can interact with a classic asset
/// (Native, AlphaNum4, or AlphaNum12). It will fail if the contractID
/// of this contract does not match the expected contractID for this asset
/// returned by Host::get_asset_contract_id_hash. This function should only be
/// called internally by the host.
///
/// No admin will be set for the Native token, so any function that checks the admin
/// (clawback, set_auth, mint, set_admin, admin) will always fail
fn init_asset(e: &Host, asset_bytes: Bytes) -> Result<(), HostError>;

Semantics: Descriptive Interface

The descriptive interface provides information about the representation of the token.

/******************************************************************************\
*
* Descriptive Interface
*
\******************************************************************************/
// Get the number of decimals used to represent amounts of this token
fn decimals() -> Result<u32, Error>;

// Get the name for this token
fn name() -> Result<String, Error>;

// Get the symbol for this token
fn symbol() -> Result<String, Error>;

Semantics: Token Interface

The token interface provides capabilities analogous to those of ERC-20 tokens.

/******************************************************************************\
*
* Token Interface
*
\******************************************************************************/
/// 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(e: &Host, from: Address, spender: Address) -> Result<i128, HostError>;

/// 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 availabe 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, sep0011_asset: String], data = [amount: i128, expiration_ledger: u32]`
fn approve(
        e: &Host,
        from: Address,
        spender: Address,
        amount: i128,
        expiration_ledger: u32,
    ) -> Result<(), HostError>;

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

/// Returns true if `id` is authorized to use its balance.
///
/// # Arguments
///
/// * `id` - The address for which token authorization is being checked.
fn authorized(env: Env, id: Address) -> bool;

/// 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, sep0011_asset: String],
/// 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, sep0011_asset: String],
/// 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, sep0011_asset: String], 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, sep0011_asset: String], data = amount:
/// i128`
fn burn_from(env: Env, spender: Address, from: Address, amount: i128);

Semantics: Admin Interface

The admin interface provides the ability to control supply and some simple compliance functionality.

/******************************************************************************\
*
* Admin Interface
*
\******************************************************************************/
/// Sets the administrator to the specified address `new_admin`.
///
/// # Arguments
///
/// * `new_admin` - The address which will henceforth be the administrator
///   of this token contract.
///
/// # Events
///
/// Emits an event with topics `["set_admin", admin: Address, sep0011_asset: String], data =
/// [new_admin: Address]`
fn set_admin(env: Env, new_admin: Address);

/// Returns the admin of the contract.
///
/// # Panics
///
/// If the admin is not set.
fn admin(env: Env) -> Address;

/// Sets whether the account is authorized to use its balance. If
/// `authorized` is true, `id` should be able to use its balance.
///
/// # Arguments
///
/// * `id` - The address being (de-)authorized.
/// * `authorize` - Whether or not `id` can use its balance.
///
/// # Events
///
/// Emits an event with topics `["set_authorized", id: Address, sep0011_asset: String], data =
/// [authorize: bool]`
fn set_authorized(env: Env, id: Address, authorize: bool);

/// Mints `amount` to `to`.
///
/// # Arguments
///
/// * `to` - The address which will receive the minted tokens.
/// * `amount` - The amount of tokens to be minted.
///
/// # Events
///
/// Emits an event with topics `["mint", admin: Address, to: Address, sep0011_asset: String], data
/// = amount: i128`
fn mint(env: Env, to: Address, amount: i128);

/// Clawback `amount` from `from` account. `amount` is burned in the
/// clawback process.
///
/// # Arguments
///
/// * `from` - The address holding the balance from which the clawback will
///   take tokens.
/// * `amount` - The amount of tokens to be clawed back.
///
/// # Events
///
/// Emits an event with topics `["clawback", admin: Address, to: Address, sep0011_asset: String],
/// data = amount: i128`
fn clawback(env: Env, from: Address, amount: i128);

Semantics: Deployment

Deploying a contract that allows interacting with Stellar classic assets

The Stellar Asset Contract can be deployed by using InvokeHostFunctionOp, with a HostFunction of type HOST_FUNCTION_TYPE_CREATE_CONTRACT. The CreateContractArgs under that will contain a ContractExecutable of type CONTRACT_EXECUTABLE_TOKEN, and a ContractIDPreimage of type CONTRACT_ID_PREIMAGE_FROM_ASSET which contains the Asset being deployed.

In order to guarantee uniqueness of contracts that allow interacting with a classic Stellar asset, the contractID for any specific Stellar asset is deterministic because the ContractIDPreimage is of type CONTRACT_ID_PREIMAGE_FROM_ASSET which contains only an Asset.

The deployment and initilization of these contracts should be atomic, and the host accomplishes this by calling init_asset during deployment of the Stellar Asset Contract.

Contracts can also deploy the Stellar Asset Contract by calling the create_asset_contract host function.

// Creates the instance of Stellar Asset Contract corresponding to the provided asset. `asset`
// is `stellar::Asset` XDR serialized to bytes format. Returns the address of the created contract.
fn create_asset_contract(asset: Object) -> Result<Object, Error>;

// This is a built-in token contract function, but only
// intended to be called by the host when deploying the Stellar Asset Contract.
// init_asset will initialize a contract that can interact with a classic asset
// (Native, AlphaNum4, or AlphaNum12). It will fail if the contractID
// of this contract does not match the expected contractID for this asset
// returned by Host::get_asset_contract_id_hash.
//
// No admin will be set for the Native token, so any function that checks the admin
// (clawback, disable_clawback, deauthorize, authorize, mint, set_admin) will always fail
fn init_asset(asset_bytes: Bytes) -> Result<(), Error>;

Balances

The Stellar Asset Contract handles balances for both ScAddress::Account and ScAddress::Contract. For the Account scenario, it uses the classic trustline for the account specified, so if the trustline doesn't exist, any function that uses it will error. For the Contract scenario, the contract will create a ContractDataEntry to hold the balance, along with an authorized and clawback flag to mimic classic trustlines and allow for the same issuer controls.

Authorized flag

Contract balances will be authorized by default unless the issuer account has AUTH_REQUIRED_FLAG set.

Auth Revocable flag

Authorization cannot be revoked with set_authorized if the issuer does not have AUTH_REVOCABLE_FLAG set.

Clawback

Account balances can only be clawed back if the trustline has TRUSTLINE_CLAWBACK_ENABLED_FLAG set. When a contract balance is created, it will be clawback enabled if the issuer account has AUTH_CLAWBACK_ENABLED_FLAG set. Note that the clawback enabled flag for contract balances cannot be modified, which is a little different from trustlines where the issuer can choose to disable it.

Design Rationale

Asset Equivalence

From the perspective of a contract, smart assets and classic assets have exactly identical semantics. This makes contract design and implementation easier by reducing the number of edge cases to consider and tests to write.

No Pre / Post Hooks

The main goal of this proposal is to create a standard asset with predictable behavior. This preserves the ability to deliver certain future enhancements such as a dedicated fee-lane for payments. If payments have arbitrary pre / post hooks then any arbitrarily expensive program could get embedded into the payment lane, significantly reducing the feasibility of such a concept.

Admin Interface

The admin is initally set to the issuer (except for the XLM contract, where no admin is set) and can be updated to a contract or a different Stellar account. The admin has controls similar to the issuer on classic as you can see in the admin interface.

No Total Supply

Total supply is not available because it isn't supported in Stellar Classic, so there's no way to reasonably track the supply between trustlines and contract balances.

Deployment

Unlike earlier variants of this proposal (see CAP-0048 and CAP-0049), this proposal does not implicitly deploy a contract for every classic asset. However, anyone can deploy the contract for any asset. The main advantage to this approach is that we no longer need to special case the identifiers for these contract. We still desire that these contracts are unique, so they will be constructed differently even though they have the same format.

No native contract for Soroban only assets

We are only adding a contract to handle Classic stellar assets. For Soroban-only assets, you'd implement a contract that follows the same interface, but will need to deploy Wasm. This will allow token contracts in the ecosystem to materialize naturally. We still have the option to nativize popular contracts in the future.

The Token Interface accepts i128s, and Contract Balances are i128s

The options considered were u63 (contained in an ScVal), u64, u128, i128, u256, and an arbitrary precision big integer. Big integer is unnecessary since u256 is more than enough to represent any reasonable balance. This is made more obvious when looking at other chains and what they use (Solana uses u64, Near uses u128, and ERC-20 uses u256). Note that a u63 is more efficient than u64 in Soroban because the u63 can be contained in a 64 bit ScVal, while u64 will need be to an ScObject, which only exists in the host and needs to be referenced by the guest.

Now the question is what should it be instead? u63 is the most efficient with u64 close behind, but not the most flexible if you use more than a couple decimal places. For example, a u64 balance with 9 decimal places would have a max value of 18,446,744,073.709551615. Stellar classic uses a signed 64 bit integer (which would be a u63 in Soroban) with seven decimal places, leading to a max value of 922,337,203,685.4775807. These values might be sufficient in most cases, but there are some edge cases where these limits are too small. For example, the US Treasury has a cash balance of $636 billion. This barely works in the Stellar case, but isn’t far off from reaching the max. It works in the variable decimals case if you give up some decimals. There are other currency and asset examples that exceed these limits as well. Instead of forcing issuers to compromise on decimals, the best option is to use u128 balances. We chose to use i128 instead though to allow contracts to handle negative amounts if they wanted to.

i128 gives users more flexibilty than Stellar classic without compromising too much in terms of performance. u256 gives us even more flexibility and easy compatibility with ERC-20, but i128 has a max value that should work with almost every use case even with 18 decimal places (170,141,183,460,469,231,731.687303715884105727).

Possible Improvement: Contract Extensibility

One disadvantage of this design is the fact that token functionality must be separated into different contracts. For example, a liquidity pool share token will typically have mint and burn functions which can be called by any user. This is not possible because there is no way to extend a contract, so the only functions available will be those specified above. Instead, the additional functions will need to be provided in a separate contract.

Protocol Upgrade Transition

Backwards Incompatibilities

This proposal is completely backwards compatible. It describes a new interface to existing behavior that is accessible from smart contracts, but it does not modify the existing behavior.

Resource Utilization

This proposal will lead to more ledger entries for tracking token state, increasing the total ledger size.

Security Concerns

This proposal does not introduce any security concerns.

Test Cases

None yet.

Implementation

None yet.