We present a new fungible token standard for NEAR Protocol which is designed to be interactive (transfers can call other contracts) and simplified and friendly in asychronous environment like NEAR native runtime.
This NEP is based on authors work on NEARswap smart contract, originating in July 2020. The NEARswap implemented a first version of this proposal in a form of multi token standard.
The only approved token standard in NEAR ecosystem is NEP-21. It's an adaptation of ERC-20 token standard from Ethereum. Both NEP-21 and ERC-20 are designed to be minimalistic and functional: provide clear interface for token transfers, allowance management (give access to token transfers to other entities or smart-contract - this is useful if other smart contract wants to withdraw it's allowance for things we buy). The ERC-20 standard is the first standard Ethereum fungible token standard and created lot of legacy. All early tooling (wallets, explorers, token templates) were build with ERC-20 in mind. Over time, the ecosystem listed many problems related to ERC-20 design:
decimals
should be required, rather than optional. This is essential to user-friendly token amount handling.- Lack of standarized metdata reference. Token should provide a resource which describes it's metadata (name, description, etc...) - this can be an URL, CID, or directly written in the contract code.
transfer
andtransferFrom
are lacking a reference (memo) argument - which is essential for compliance reasons. We need to be able to be able to link the transfer to an an external document (eg: invoice), order ID or other on-chain transaction or simply set a transfer reason.- Direct
transfers
to smart-contract in general is an error and should be protected. Both in Ethereum in NEAR, smart-contracts are NOT notified if someone is sending them tokens. This causes funds lost, token locked and many critical misbehavior. - Avoid problems mentioned in the previous point, all transfers should be done through
approve
(allowance creation) andtransferFrom
, which is less intuitive and makes UX more complex: not only we need to create and keep track of right allowance (with all edge cases: user creates allowance, but token is not callingtransferFrom
and makes user to create another allowance). - Fees calculation. With
approve
+transferFrom
, the business provider has to make an additional transaction (transferFrom) and calculate it in the operation cost.
There are few articles analyzing ERC-20 flaws (and NEP-21): What’s Wrong With ERC-20 Token?, Critical problems of ERC20 token standard.
And the NEP-110 discussion: near/NEPs#110 addressing same issues in a bit different way.
- NEP-110 Advanced Fungible Token Standard
- NEP-122 Allowance-free vault-based token standard
- ERC-20
- ERC-223
- ERC-777
We propose a new token standard to solve issues above. The design goals:
- not trading off simplicity - the contract must be easy to implement and use
- completely remove allowance: removes UX flaws and optimize contract storage space
- simplify interaction with other smart-contracts
- simplify flow in NEP-122
- remove frictions related to different decimals
- enable smart contract composability through token transfers
Our work is mostly influenced by the aforementioned ERC-223 and NEP-122: Allowance-free vault-based token standard.
To simplify the interface we and make sure we send funds only to accounts which agree to receive token of a given type we introduce account registration concept. NEP-21 transfer (and approval) functions require payment to cover the potential storage fees. Each method which calls a #payment
method must be marked with #payment
. This complicates the tools design - they will need to ask user to attach payment for every such transaction. With _account registration, an account must firstly to opt-in to a token, and only then it can receive tokens. This gives many benefits:
- protects account for receiving unwanted tokens (spamming, compliance)
- signals that a smart-contract can handle tokens
- removes a need for attaching payment to every
transfer
call, and every function which transitively callstransfer
.
An account, once registered, can opt-out - with that, a storage deposit will be returned, and the account will stop accepting token transfers.
Instead of having approve
+ transferFrom
, we propose a transfer_call
function which transfer funds and calls external smart-contract to notify him about the transfer. This function essentially requires that a recipient must implement TransferCallRecipient
interface described below.
We add a required memo
argument to all transfer functions. Similarly to bank transfer and payment orders, the memo
argument allows to reference transfer to other event (on-chain or off-chain). It is a schema less, so user can use it to reference an external document, invoice, order ID, ticket ID, or other on-chain transaction. With memo
you can set a transfer reason, often required for compliance.
This is also useful and very convenient for implementing FATA (Financial Action Task Force) guidelines (section 7(b) ). Especially a requirement for VASPs (Virtual Asset Service Providers) to collect and transfer customer information during transactions. VASP is any entity which provides to a user token custody, management, exchange or investment services. With ERC-20 (and NEP-21) it is not possible to do it in atomic way. With memo
field, we can provide such reference in the same transaction and atomically bind it to the money transfer.
Lack of decimals creates difficulty for handling user experience in all sorts of tools
As mentioned in the Rationale, for interactions with tools and assure good UX, we need to know the base of token arithmetic.
ERC-20 has an optional decimals
contract attribute, other Ethereum standards makes this attribute obligatory. We follow the ERC-777 proposal, and fix the decimals once and for all:
- Each token should have 18 digits precision (decimals), same as most of the existing Ethereum tokens. If a token contract returns a balance of 500,000,000,000,000,000 (0.5e18) for a user, the user interface MUST show 0.5 tokens to the user.
- We port the
granularity
concept from ERC-777: the smallest part of the token that’s (denominated in e18) not divisible. The following rules MUST be applied regarding the granularity: - The granularity value MUST be set at creation time.
- The granularity value MUST NOT be changed, ever.
- The granularity value MUST be greater than or equal to 1.
- All balances MUST be a multiple of the granularity.
- Any amount of tokens (in the internal denomination) minted, sent or burned MUST be a multiple of the granularity value.
- Any operation that would result in a balance that’s not a multiple of the granularity value MUST be considered invalid, and the transaction MUST revert.
In synchronous like environment (Ethereum EVM and all it's clones), reactive calls (like transfer_call
, or transfer
from ERC223) are susceptible for reentrancy attacks. In the discussion below lets denote a transaction for contract A
which calls external smart contract B
(we write A->B
).
An attack vector is to call back the originating smart-contract (B->A
) in the same transaction - we call it reentrancy. This creates various issues since the reentrance call is happening before all changes have been committed and it's not isolated from the originating call. This leads to many exploits which have been widely discussed and audited.
In asynchronous environment like NEAR, an external smart contract call execution is happening in a new, isolated routine once the originating call finished and all state changes have been committed. This eliminates the reentrancy - any call from external smart contract back to the originating smart contract (A->B->A
) is isolated from the originating smart-contract. The attack vector is limited and essentially it's "almost" reduced to other attacks happening in separate transaction. Almost - because a user still need to manage the callbacks.
If a recipient of transfer_call
fails, we would like to preserve the tokens from being lost. For that, a token MAY implement pattern developed by NEP-110: when sending tokens through transfer_call
, append a finalize_token_call
callback promise to the on_ft_receive
call. This callback will check if the previous one was successful, and if not, it will rollback the transfer.
You can check the NEP-110 handle_token_received
implementation (the NEP-110 reference implementation uses handle_token_received
for function name instead of finalize_token_call
). This function shouldn't be called externally.
We propose to standarize the final call back:
- Use
finalize_token_call
as a function name - When scheduling a call to
recipient.on_ft_receive
, don't pass all NEAR for fees. Reserve some NEAR forfinalize_token_call
to make sure we will be able to handle both scenarios: whenon_ft_receive
succeeds and when it fails.
NEP-110 stores metadata on chain, and defines a structure for the metadata:
struct Metadata {
name: String, // token name
symbol: String, // token symbol
web_link: String, // URL to the human readable page about the token
metadata_link: String, // URL to the metadata file with more information about the token, like different icon sets
}
We adopt this concept, but relax on it's content. Metadata should be as minimal as possible. We combine it with other token related data, and require that a contract will have following attributes:
struct Metadata {
name: String, // token name
symbol: String, // token symbol
reference: String, // URL to additional resources about the token.
granularity: uint8,
decimals = 18,
}
We improve the token NEP-110 design by:
- handling compliance issues
- solving UX issues related to decimals
- clear support for smart-contract and basic transfers
We improve the NEP-122 design by:
- simplifying the flow (no need to create safe locks) and less callbacks
- handling compliance issues
- solving UX issues related to decimals
We improve the NEP-21 design by:
- all points mentioned above
- greatly simplifying implementation
- reducing the storage size (no need to store allowances)
- making the transfer interactive: being able to notify the recipient smart contract for the purchase / transfer
- enabling smart contract composability through token transfers (NEP-21 smart-contract can't react on a token transfer).
The pay to smart-contract flow (approve
+ tranferFrom
), even though it's not very user friendly and prone for wrong allowance, it's very simple. It moves a complexity of handling token-recipient interaction from the contract implementation to the recipient. This makes the contract design simpler and more secure in domains where reentrancy attack is possible.
Please look at the source code for more details and comments.
struct Metadata {
name: String, // token name
symbol: String, // token symbol
reference: String, // URL to additional resources about the token.
granularity: uint8, // the smallest part of the token that’s (denominated in e18) not divisible
decimals: uint8, // MUST be 18,
}
pub trait TransferCallRecipient {
fn metadata() -> Metadata;
fn total_supply(&self) -> U128;
fn balance_of(&self, token: AccountId, holder: AccountId) -> U128;
fn transfer(&mut self, recipient: AccountId, amount: U128, msg: String, memo: String) -> bool;
fn transfer_call(
&mut self,
recipient: AccountId,
amount: U128,
msg: String,
memo: String,
) -> bool;
/// Registers the caller
#[payable]
fn register_account(&mut self);
/// Unregisters the caller
#[payable]
fn unregister_account(&mut self);
}
pub trait TransferCallRecipient {
fn on_ft_receive(&mut self, token: AccountId, from: AccountId, amount: U128, msg: String);
}
- Extend this token standard for _operator functionality as defined in ERC-777. This should be backward compatible extension.