Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fungible Token standard #21

Merged
merged 8 commits into from
May 15, 2020
Merged

Fungible Token standard #21

merged 8 commits into from
May 15, 2020

Conversation

evgenykuzyakov
Copy link
Contributor

@evgenykuzyakov evgenykuzyakov commented Oct 29, 2019

Fungible Token

Summary

A standard interface for fungible tokens allowing for ownership, escrow and transfer, specifically targeting third-party marketplace integration.

Motivation

NEAR Protocol uses an asynchronous sharded Runtime. This means the following:

  • Storage for different contracts and accounts can be located on the different shards.
  • Two contracts can be executed at the same time in different shards.

While this increases the transaction throughput linearly with the number of shards, it also creates some challenges for cross-contract development.
For example, if one contract wants to query some information from the state of another contract (e.g. current balance), by the time the first contract receive the balance the real balance can change.
It means in the async system, a contract can't rely on the state of other contract and assume it's not going to change.

Instead the contract can rely on temporary partial lock of the state with a callback to act or unlock, but it requires careful engineering to avoid dead locks.
In this standard we're trying to avoid enforcing locks, since most actions can still be completed without locks by transferring ownership to an escrow account.

Prior art:

Guide-level explanation

We should be able to do the following:

  • Initialize contract once. The given total supply will be owned by the given account ID.
  • Get the total supply.
  • Transfer tokens to a new user.
  • Set a given allowance for an escrow account ID.
    • Escrow will be able to transfer up this allowance from your account.
    • Get current balance for a given account ID.
  • Transfer tokens from one user to another.
  • Get the current allowance for an escrow account on behalf of the balance owner. This should only be used in the UI, since a contract shouldn't rely on this temporary information.

There are a few concepts in the scenarios above:

  • Total supply. It's the total number of tokens in circulation.
  • Balance owner. An account ID that owns some amount of tokens.
  • Balance. Some amount of tokens.
  • Transfer. Action that moves some amount from one account to another account.
  • Escrow. A different account from the balance owner who has permission to use some amount of tokens.
  • Allowance. The amount of tokens an escrow account can use on behalf of the account owner.

Note, that the precision is not part of the default standard, since it's not required to perform actions. The minimum
value is always 1 token.

Simple transfer

Alice wants to send 5 wBTC tokens to Bob.

Assumptions

  • The wBTC token contract is wbtc.
  • Alice's account is alice.
  • Bob's account is bob.
  • The precision on wBTC contract is 10^8.
  • The 5 tokens is 5 * 10^8 or as a number is 500000000.

High-level explanation

Alice needs to issue one transaction to wBTC contract to transfer 5 tokens (multiplied by precision) to Bob.

Technical calls

  1. alice calls wbtc::transfer({"new_owner_id": "bob", "amount": "500000000"}).

Token deposit to a contract

Alice wants to deposit 1000 DAI tokens to a compound interest contract to earn extra tokens.

Assumptions

  • The DAI token contract is dai.
  • Alice's account is alice.
  • The compound interest contract is compound.
  • The precision on DAI contract is 10^18.
  • The 1000 tokens is 1000 * 10^18 or as a number is 1000000000000000000000.
  • The compound contract can work with multiple token types.

High-level explanation

Alice needs to issue 2 transactions. The first one to dai to set an allowance for compound to be able to withdraw tokens from alice.
The second transaction is to the compound to start the deposit process. Compound will check that the DAI tokens are supported and will try to withdraw the desired amount of DAI from alice.

  • If transfer succeeded, compound can increase local ownership for alice to 1000 DAI
  • If transfer fails, compound doesn't need to do anything in current example, but maybe can notify alice of unsuccessful transfer.

Technical calls

  1. alice calls dai::set_allowance({"escrow_account_id": "compound", "allowance": "1000000000000000000000"}).
  2. alice calls compound::deposit({"token_contract": "dai", "amount": "1000000000000000000000"}). During the deposit call, compound does the following:
    1. makes async call dai::transfer_from({"owner_id": "alice", "new_owner_id": "compound", "amount": "1000000000000000000000"}).
    2. attaches a callback compound::on_transfer({"owner_id": "alice", "token_contract": "dai", "amount": "1000000000000000000000"}).

Multi-token swap on DEX

Charlie wants to exchange his wLTC to wBTC on decentralized exchange contract. Alex wants to buy wLTC and has 80 wBTC.

Assumptions

  • The wLTC token contract is wltc.
  • The wBTC token contract is wbtc.
  • The DEX contract is dex.
  • Charlie's account is charlie.
  • Alex's account is alex.
  • The precision on both tokens contract is 10^8.
  • The amount of 9001 wLTC tokens is Alex wants is 9001 * 10^8 or as a number is 900100000000.
  • The 80 wBTC tokens is 80 * 10^8 or as a number is 8000000000.
  • Charlie has 1000000 wLTC tokens which is 1000000 * 10^8 or as a number is 100000000000000
  • Dex contract already has an open order to sell 80 wBTC tokens by alex towards 9001 wLTC.
  • Without Safes implementation, DEX has to act as an escrow and hold funds of both users before it can do an exchange.

High-level explanation

Let's first setup open order by Alex on DEX. It's similar to Token deposit to a contract example above.

  • Alex sets an allowance on wBTC to DEX
  • Alex calls deposit on Dex for wBTC.
  • Alex calls DEX to make an new sell order.

Then Charlie comes and decides to fulfill the order by selling his wLTC to Alex on DEX.
Charlie calls the DEX

  • Charlie sets the allowance on wLTC to DEX
  • Alex calls deposit on Dex for wLTC.
  • Then calls DEX to take the order from Alex.

When called, DEX makes 2 async transfers calls to exchange corresponding tokens.

  • DEX calls wLTC to transfer tokens DEX to Alex.
  • DEX calls wBTC to transfer tokens DEX to Charlie.

Technical calls

  1. alex calls wbtc::set_allowance({"escrow_account_id": "dex", "allowance": "8000000000"}).
  2. alex calls dex::deposit({"token": "wbtc", "amount": "8000000000"}).
    1. dex calls wbtc::transfer_from({"owner_id": "alex", "new_owner_id": "dex", "amount": "8000000000"})
  3. alex calls dex::trade({"have": "wbtc", "have_amount": "8000000000", "want": "wltc", "want_amount": "900100000000"}).
  4. charlie calls wltc::set_allowance({"escrow_account_id": "dex", "allowance": "100000000000000"}).
  5. charlie calls dex::deposit({"token": "wltc", "amount": "100000000000000"}).
    1. dex calls wltc::transfer_from({"owner_id": "charlie", "new_owner_id": "dex", "amount": "100000000000000"})
  6. charlie calls dex::trade({"have": "wltc", "have_amount": "900100000000", "want": "wbtc", "want_amount": "8000000000"}).
    • dex calls wbtc::transfer({"new_owner_id": "charlie", "amount": "8000000000"})
    • dex calls wltc::transfer({"new_owner_id": "alex", "amount": "900100000000"})

Reference-level explanation

The full implementation in Rust can be found there: https://github.com/nearprotocol/near-sdk-rs/blob/master/examples/fungible-token/src/lib.rs

NOTES:

  • All amounts, balances and allowance are limited by U128 (max value 2**128 - 1).
  • Token standard uses JSON for serialization of arguments and results.
  • Amounts in arguments and results have are serialized as Base-10 strings, e.g. "100". This is done to avoid
    JSON limitation of max integer value of 2**53.

Interface:

/******************/
/* CHANGE METHODS */
/******************/

/// Sets the `allowance` for `escrow_account_id` on the account of the caller of this contract
/// (`predecessor_id`) who is the balance owner.
pub fn set_allowance(&mut self, escrow_account_id: AccountId, allowance: U128);

/// Transfers the `amount` of tokens from `owner_id` to the `new_owner_id`.
/// Requirements:
/// * `amount` should be a positive integer.
/// * `owner_id` should have balance on the account greater or equal than the transfer `amount`.
/// * If this function is called by an escrow account (`owner_id != predecessor_account_id`),
///   then the allowance of the caller of the function (`predecessor_account_id`) on
///   the account of `owner_id` should be greater or equal than the transfer `amount`.
pub fn transfer_from(&mut self, owner_id: AccountId, new_owner_id: AccountId, amount: U128);


/// Transfer `amount` of tokens from the caller of the contract (`predecessor_id`) to
/// `new_owner_id`.
/// Act the same was as `transfer_from` with `owner_id` equal to the caller of the contract
/// (`predecessor_id`).
pub fn transfer(&mut self, new_owner_id: AccountId, amount: U128);

/****************/
/* VIEW METHODS */
/****************/

/// Returns total supply of tokens.
pub fn get_total_supply(&self) -> U128;

/// Returns balance of the `owner_id` account.
pub fn get_balance(&self, owner_id: AccountId) -> U128;

/// Returns current allowance of `escrow_account_id` for the account of `owner_id`.
///
/// NOTE: Other contracts should not rely on this information, because by the moment a contract
/// receives this information, the allowance may already be changed by the owner.
/// So this method should only be used on the front-end to see the current allowance.
pub fn get_allowance(&self, owner_id: AccountId, escrow_account_id: AccountId) -> U128;

Drawbacks

  • Current interface doesn't have minting, precision (decimals), naming. But it should be done as extensions, e.g. a Precision extension.
  • It's not possible to exchange tokens without transferring them to escrow first.
  • It's not possible to transfer tokens to a contract with a single transaction without setting the allowance first.
    It should be possible if we introduce transfer_with function that transfers tokens and calls escrow contract. It needs to handle result of the execution and contracts have to be aware of this API.

Future possibilities

  • Support for multiple token types
  • Minting and burning
  • Precision, naming and short token name.

@evgenykuzyakov
Copy link
Contributor Author

Feedback by DanielRX:

First thing that comes to mind is that this isn't an interface alone?
It mentions that on deployment all tokens are give to the userID
Why should that matter for the purposes of an interface between tokens and consuming contracts?

That's a good point. The standard doesn't need to define initialization function.

Following the logic, ERC-20 shouldn't define names and symbols, cause it doesn't affect other contracts or how they interact with ERC-20.

It also feels like its bloated, the base standard(to parallel erc20) surely should contain only the transfer and approve stuff, not locking and unlocking and timers and all of that?

Locks are important for the async functionality. Without locks, it's not possible to have an escrow DEX from the 3rd example. The 3rd examples is a building block on how to interact with 2 token contracts at the same time.

It's possible to avoid lock/unlock and use temporary transfers to an escrow account itself, but it affects the allowance in case of rollback. For example:

  • DEX temporary moves both tokens wBTC and wLTC to itself
  • One of the transfers fails.
  • DEX reverts the successful transfer back to the original owner

Now the DEX allowance of the reverted original owner has decreased. So the DEX can't try to transfer again. And the owner has to reset the allowance back.

@DanielRX
Copy link

Following the logic, ERC-20 shouldn't define names and symbols, cause it doesn't affect other contracts or how they interact with ERC-20

Names and symbols do affect other contracts and how they interact, they are public interfaces.

If the name was internal, it wouldn't need to define them as having any meaning, but they are defined so that consumers (markets, web UIs, other contracts) can know that if it is ERC20, the name, symbol, total supply, etc mean a certain thing.

Locks are important for the async functionality. Without locks, it's not possible to have an escrow DEX from the 3rd example. ... And the owner has to reset the allowance back.

I'm not saying "Locks shouldn't be standardized", but should all tokens, regardless of their usage, have to implement locking? The base standard (as I said), should be the intersection of all Fungible Token use cases

Having "fungible tokens" is having tokens that can be transferred. Arguably you could say approvals fall into this, but locking is not part of a fungible token, it's part of a Lockable or a Async token

The key point is that locking is not specific to NFTs or FTs, so putting it into the FT standard, makes it less modular than say, having a "lockable" token standard, that NFTs and FTs should adhere to for best support

@evgenykuzyakov
Copy link
Contributor Author

evgenykuzyakov commented Oct 29, 2019

DanielRX | StardustToday at 11:13 AM
I've commented there, but my point isnt about "making it work in async"
You don't define interfaces with complex functionality expected for all tokens
Plus as I commented, lockable tokens are therefore just tokens, and theres no lockable standard to look to for other uses
Having lockable contracts, tokens, escrows, etc, is a use case, and making the standard for a locking interface, would allow the tokens to then inherit this
 
Evgeny | NEARToday at 11:20 AM
Locking acts differently for NFT and FT because they have slightly different interfaces, but it's important have similar locking functionality on both standards. Without locking, we can go for transfer with a callback approach to avoid approvals at all.
We can't have same lockable standard on both NFT and FT interfaces
Is there an alternative to non-lockable async FT?

DanielRX | StardustToday at 11:25 AM
Yes, ERC20
You are assuming you must make it support escrows in the base interface
I'm not saying "remove locks from tokens", I'm saying it should be layered

Base FT
Lockable FT - Mintable FT - Burnable FT
FTs that require a mix of layer 2

And which members of the lockable interface can't be consolidated for NFTs and FTs?
If I want a barebones FT, why must I add locking? Having a smaller interface allows for greater compat

If I want a "multi transfer" contract, all it needs is transfer on the token contracts
The reason I did not put this on GH, is that it's a discussion, and the key points after the discussion serve being put on the NEP for future understanding
 
Evgeny | NEARToday at 11:52 AM
Thanks, that makes sense

@DanielRX
Copy link

Since my comments about not putting the entire discussion on GH were ignored, I will put them here myself.

Why should the base token interface contain locking? The same can be said for minting (and why ERC20 is not a mintable token by default)

Why can't the base token avoid the locking, and build a second token standard on top of that?

Is locking needed for fungible tokens, or merely for "Market Integrated Tokens", "Escrow Tokens" or "Lockable tokens"?

@evgenykuzyakov
Copy link
Contributor Author

@DanielRX I hear your feedback about splitting lockable from the base transferable token. Will update this NEP to split the interface into 3 parts: basic (includes only transfer), escrow (enables allowance and transferFrom, and lockable (enables locks)).

The reason I put entire conversation on github is to keep the full discussion for other reviewers. Please let me know through DM if you don't want to put your comments on github. I'll update it.

@evgenykuzyakov
Copy link
Contributor Author

There were no updates for this NEP for a while. We're working on upgrading the lock design, so the exchange doesn't have explicitly unlock the token. See #23 and #24 for proposals

@behaviary
Copy link

The progress on what is blocking this can be viewed: #26

@evgenykuzyakov
Copy link
Contributor Author

I've updated the standard (removed locks), added the spec. Needs review

Copy link

@behaviary behaviary left a comment

Choose a reason for hiding this comment

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

Needs SAFEs. Just adding this to kill notification until we are implementing that.

@evgenykuzyakov
Copy link
Contributor Author

Needs SAFEs. Just adding this to kill notification until we are implementing that.

@potatodepaulo
This standard doesn't need SAFEs. It's still usable with ownership (see examples)

@render
Copy link

render bot commented May 11, 2020

@render
Copy link

render bot commented May 11, 2020

Your Render PR Server at https://nomicon-pr-21.onrender.com is now live!

View it on your dashboard at https://dashboard.render.com/static/srv-bqsq7m1j38far37cn5j0.

Copy link

@behaviary behaviary left a comment

Choose a reason for hiding this comment

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

Concerns:

  1. Escrow is technically required, but we don't have a fleshed out standards. Would you advocate for the simplest use case to just implement their own escrow?
  2. We address that get_balance is unreliable since state can change between queries. I think allowances are acceptable. But this points to problem one, which is that there is an implied standard for escrows.

@evgenykuzyakov
Copy link
Contributor Author

@potatodepaulo, there are 2 escrow examples:

  • Token deposit to a contract
  • Multi-token swap on DEX

Escrow acts just like a regular account that holds tokens. It maintains internal mapping between token holders and their corresponding balances. The spec for escrow is not required for the standard.
You can think about escrow contract as common contracts on ethereum, e.g. compound or uniswap pools. They hold tokens for you, instead of your account. This means you can't double spent your tokens through multiple allowances. It also means you can't list the same tokens on multiple exchanges (as was supported with safes).

@behaviary
Copy link

Update: Talked with @evgenykuzyakov. We will abstract much of the use case specific parts of fungible token standard to other standard (for instance Escrow, and MetaData).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants