Skip to content

Latest commit

 

History

History
525 lines (406 loc) · 21.8 KB

cap-0046-06.md

File metadata and controls

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

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 tokens which is suitable for both smart tokens and wrapped classic tokens. The interface tries to follow an ERC-20 model, although functions perform their own authorization more like EIP-2612 permit. Identifiers in these tokens can be contracts, ed25519 public keys, or Stellar accounts.

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. The comment above each type describes the way in which the type is serialized as an SCVal.

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

pub struct Ed25519Signature {
    pub public_key: BytesN<32>,
    pub signature: BytesN<64>,
}

pub struct AccountSignatures {
    pub account_id: AccountId,
    pub signatures: Vec,
}

pub enum Signature {
    Invoker,
    Ed25519(Ed25519Signature),
    Account(AccountSignatures),
}

pub enum Identifier {
    Contract(BytesN<32>),
    Ed25519(BytesN<32>),
    Account(AccountId),
}

// Byte arrays and strings can be passed to contracts and stores using the Bytes type.
// BytesN is fixed size, while Bytes is variable
pub struct Bytes(EnvVal<Host, Object>);
pub struct BytesN<const N: usize>(EnvVal<Host, Object>);

Semantics: Signatures

Signatures for this contract are over the following SignaturePayload type

pub struct SignaturePayloadV0 {
    pub network: Bytes,
    pub contract: BytesN<32>,
    pub name: Symbol,
    pub args: Vec,
}

pub enum SignaturePayload {
    V0(SignaturePayloadV0),
}

Semantics: Initialization

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

// Initializes a token contract that does not wrap an asset on the classic side.
// Should be used on a contract created with create_token_from_contract.
// Sets admin, decimals, name, symbol, and the clawback flag.
fn init(admin: Identifier, metadata: TokenMetadata, can_clawback: bool) -> Result<(), Error>;

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<Bytes, Error>;

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

Semantics: Token Interface

TODO: Specify u128 in CAP-0046

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

/******************************************************************************\
*
* Token Interface
*
\******************************************************************************/
// Get the allowance for "spender" to transfer from "from"
fn allowance(from: Identifier, spender: Identifier) -> Result<u128, Error>;

// Verify from and nonce. Then increase the allowance by "amount" for "spender" to transfer from "from".
// If an allowance does not exist, set allowance to "amount".
fn increase_allowance(from: Signature, nonce: BigInt, spender: Identifier, amount: u128) -> Result<(), Error>;

// Verify from and nonce. Then decrease the allowance by "amount" for "spender" to transfer from "from".
// If amount is greater than the existing allowance, set the allowance to 0.
fn decrease_allowance(from: Signature, nonce: BigInt, spender: Identifier, amount: u128) -> Result<(), Error>;

// Get the balance of "id"
fn balance_of(id: Identifier) -> Result<u128, Error>;

// Verify from and nonce. Then transfer "amount" from "from" to "to"
fn xfer(from: Signature, nonce: BigInt, to: Identifier, amount: u128) -> Result<(), Error>;

// Verify spender and nonce. Then transfer "amount" from "from" to "to", consuming the allowance of "spender"
fn xfer_from(spender: Signature, nonce: BigInt, from: Identifier, to: Identifier, amount: u128) -> Result<(), Error>;

Semantics: Admin Interface

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

/******************************************************************************\
*
* Admin Interface
*
\******************************************************************************/
// If "admin" is the administrator and clawback is not disabled, clawback "amount" from "from".
// The "amount" is burned. This function can be used on deauthorized balances. 
fn clawback(admin: Signature, nonce: BigInt, from: Identifier, amount: u128) -> Result<(), Error>;

// If "admin" is the administrator, clear the authorized flag for "id"
fn deauthorize(admin: Signature, nonce: BigInt, id: Identifier) -> Result<(), Error>;

// If "admin" is the administrator, set the authorized flag for "id". Without authorization,
// "id" will not be able to use it's balance.
fn authorize(admin: Signature, nonce: BigInt, id: Identifier) -> Result<(), Error>;

// Returns true if the authorized flag is set
fn is_authorized(id: Identifier) -> Result<bool, Error>;

// If "admin" is the administrator, mint "amount" to "to"
fn mint(admin: Signature, nonce: BigInt, to: Identifier, amount: u128) -> Result<(), Error>;

// If "admin" is the administrator, set the administrator to "id"
fn set_admin(admin: Signature, nonce: BigInt, new_admin: Identifier) -> Result<(), Error>;

// If "admin" is the administrator, disable the clawback function for this contract 
fn disable_clawback(admin: Signature, nonce: BigInt) -> Result<(), Error>;

// Returns the current admin. If admin is not set, returns a zero ed25519 key
fn admin() -> Result<Identifier, Error>;

Semantics: Classic Wrapper Interface

The wrapper interface is only provided for classic assets, allowing assets to flow between classic and smart.

/******************************************************************************\
*
* Wrapper Interface
*
\******************************************************************************/
// Move "amount" from "id" on classic to "id" on smart
fn import(id: Signature, nonce: BigInt, amount: i64) -> Result<(), Error>;

// Move "amount" from "id" on smart to "id" on classic
fn export(id: Signature, nonce: BigInt, amount: i64) -> Result<(), Error>;

Semantics: Deployment

Deploying a contract that allow wrapping Stellar classic assets

In order to guarantee uniqueness of wrapper contracts, create_token_from_asset produces a deterministic contract identifier that does not depend on the creator or any salt. This is achieved by introducing ENVELOPE_TYPE_CONTRACT_ID_FROM_ASSET in CAP-0046-02. The wrapper contracts should be initialized atomically with contract creation so the right asset is passed in. create_token_from_asset should call the init_asset function specified below after the contract is created.

Note that both functions below are not exposed for contracts to call. create_token_from_asset can only be called through InvokeHostFunctionOp specified in CAP-0046-04.

// Creates a contract that wraps a classic Stellar asset. The asset parameter should be
// the binary representation of the XDR Asset being wrapped. This will also call init_asset on the
// newly created contract to prevent the predetermined contract id for this asset from being taken.
fn create_token_from_asset(asset: Object) -> Result<Object, Error>;

// This is a built-in token contract function, but only
// intended to be called by create_token_from_asset.
// init_asset will initialize a contract for a wrapped 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_contract_id_from_asset. This function should only be
// called by the create_token_from_asset host function for this reason.
//
// 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>;

Deploying a token contract that only operates on Soroban

// From CAP-0046-02 - Instantiates a contract with the source referring to the built-in token.
// Returns the newly created contractID.
fn create_token_from_contract(salt: Object) -> Result<Object, Error>;

Uniqueness does not apply to non-wrapper token contracts, so this function uses the creating contract and a salt.

Authorized flag

Balances will be authorized by default.

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.

Several Authorization Mechanisms

This proposal supports several signature mechanisms to acknowledge the reality that account-based multisig is a fundamental aspect of the Stellar ecosystem. I anticipate many pure contract users will favor a single Ed25519 key because fees will be lower, so we provide this option as well. Contracts cannot sign, so they need a separate authorization mechanism too (achieved with get_invoking_contract).

Message Allows Caller to Be Specified

This is an extremely useful design point that makes it possible to couple multiple signed messages. Consider the interface described in "Ecosystem Support: Wrap-Then-Do Contract". This interface can receive multiple signed messages. But an attacker could observe this on the network, then submit the signed messages separately. By adding the caller to the signed message, it allows the caller to perform additional consistency checks. For example, the calling contract could receive a signature over a message containing the coupled signed messages.

Admin Interface

Unlike the existing Stellar protocol, which uses simple flags for compliance controls, this proposal delegates all decisions regarding administrative functionality to an administrator. The administrator can be a contract, a simple Ed25519 key, or an existing Stellar account.

This design decision does not break compatibility with Stellar assets. If an existing holder does not want to accept the new terms, they simply elect not to wrap their asset. The ability to do this without breaking compatibility is one of the big advantages of using wrappers.

If people want an analog to the existing compliance semantics, we can provide a reference administrator contract that implements them.

Classic Wrapper Interface

The wrapper interface is only provided for classic assets. Smart assets do not have a wrapper interface because those administered by contracts would lose control over the wrapped assets. Smart asset issuers can always deploy their own wrapper interface should they need it.

No Total Supply

Total supply is confusing for classic assets because they can exist in wrapped and unwrapped form. Total supply is also high contention when minting and burning. Tokens that need to track total supply can do so by having the administrator contract update it when calling mint and clawback.

Deployment

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

Uniqueness is not a concern for smart tokens, so their identifiers are constructed using the typical creator and salt approach.

Clawback

The ability to call the clawback function is set by the entity that calls init for non-classic tokens. For classic tokens, clawback will be enabled only if AUTH_CLAWBACK_ENABLED_FLAG is set on the issuer. If the issuer account is missing, clawback will be disabled on initialization because anyone could create the missing issuer account without any flags. Instead of checking for AUTH_CLAWBACK_ENABLED_FLAG, we could just have clawback enabled by default, and allow the issuer to clear it. I don't think this is a great idea because I think we'll see most issuers not clear the flag even if they don't require clawback, making users hesitant to use classic assets on Soroban.

Note that clawback can be disabled using disable_clawback, but it can not be enabled. This matches the clawback behavior on Stellar classic.

Balances are u128s

The options considered were u63 (contained in an ScVal), u64, u128, 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.

u128 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 u128 has a max value that shold work with almost every use case even with 18 decimal places (340,282,366,920,938,463,463.374607431768211456). The performance implications of using u128 instead of u63 or u64 should be measured.

No approve function

This proposal does not include an approve function like the one specified in EIP-20. This was done to mitigate the allowance vulnerability specified in this google doc. TLDR: If user A specified an allowance of 10 for B, then tries to reduce it to 5, B could spend the allowance before the reduction is accepted on the ledger, and then have an additional 5 to spend. Instead of reducing the allowance to 5, the user actually increased it to 15!

Instead of approve, this proposal specifies two functions to deal with allowances - increase_allowance and decrease_allowance.

No explicit functions to burn

This proposal does not include a burn or burnFrom function that allows a user to burn a balance that it holds. This functionality can be imitated by just sending the balance to the zero ed25519 identifier. The alternative would be to implement burn and burnFrom similar to what the OpenZeppelinERC-20 implementation does.

Ecosystem Support: Wrap-Then-Do Contract

One concern about the use of wrapped assets is that it will degrade the user experience by requiring users of classic assets to submit additional transactions: first wrap, then do. This can be avoided relatively easily by implementing a generic wrap-then-do contract. A wrap-then-do contract might have the following functions

fn do_then_unwrap(sig: KeyedAccountAuthorization, token: u256,
                  id: AccountAuthorization, amount: u128, contract: u256,
                  symbol: SCSymbool, parameters: SCVec) {
    // check that "sig" contains medium threshold signatures over
    //     Message::V0(MessageV0 {
    //         nonce: nonce_of(Identifier::Account(sig.publicKey)),
    //         thisContract: get_contract_id(),
    //         symbol: "do_then_unwrap",
    //         parameters: (token, id, amount, contract, symbol, symbol,
    //                      parameters),
    //         caller: Caller::Transaction,
    //     })
    // otherwise trap

    let keyedID = KeyedAccountAuthorization {
        publicKey: sig.publicKey,
        authorization: id;
    };
    call(contract, symbol, parameters);
    call(token, "unwrap", (keyedID, amount));
}

// signatures in "id" should be over messages that specify this contract as the
// "caller"
// signatures in elements of "parameters" should be over messages that specify
// this contract as the "caller"
fn wrap_then_do(sig: KeyedAccountAuthorization, token: u256,
                id: AccountAuthorization, amount: u128, contract: u256,
                symbol: SCSymbool, parameters: SCVec) {
    // check that "sig" contains medium threshold signatures over
    //     Message::V0(MessageV0 {
    //         nonce: nonce_of(Identifier::Account(sig.publicKey)),
    //         thisContract: get_contract_id(),
    //         symbol: "wrap_then_do",
    //         parameters: (token, id, amount, contract, symbol, symbol,
    //                      parameters),
    //         caller: Caller::Transaction,
    //     })
    // otherwise trap

    let keyedID = KeyedAccountAuthorization {
        publicKey: sig.publicKey,
        authorization: id;
    };
    call(token, "wrap", (keyedID, amount));
    call(contract, symbol, parameters);
}

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.