-
Notifications
You must be signed in to change notification settings - Fork 141
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 Core Standard #141
Comments
first off - thanks @evgenykuzyakov for kicking this off I propose we take the following iterative approach to keep the online discussion manageable and progressing towards the end goal
Deliverable GoalsThe end product should be a Rust library module published on https://crates.io/. Thoughts? I will be submitting my first proposed change shortly ... |
ACCEPTED: Change Proposal: namespace interface function names
|
REJECTED: Change Proposal: Replace primitive types with domain specific wrappersREJECTED REASON: the proposal belongs in Rust reference implementation On the wire, the underlying types will be marshalled in low level primitive format to be as efficient as possible. However, the contract interface should be strongly domain typed to clearly express purpose. I propose the following domain types:
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct TokenAmount(pub U128);
impl From<u128> for TokenAmount {
fn from(value: u128) -> Self {
Self(value.into())
}
}
impl TokenAmount {
pub fn value(&self) -> u128 {
self.0 .0
}
}
impl Deref for TokenAmount {
type Target = u128;
fn deref(&self) -> &Self::Target {
&self.0 .0
}
}
impl DerefMut for TokenAmount {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 .0
}
}
impl Display for TokenAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0 .0.fmt(f)
}
}
impl PartialOrd for TokenAmount {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value().partial_cmp(&other.value())
}
}
impl Ord for TokenAmount {
fn cmp(&self, other: &Self) -> Ordering {
self.value().cmp(&other.value())
}
}
impl Eq for TokenAmount {}
/// > 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.
///
/// - https://github.com/near/NEPs/issues/136
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct Memo(pub String);
impl Deref for Memo {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Memo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// > The mint, send and burn processes can all make use of a data and operatorData fields which are
/// > passed to any movement (mint, send or burn). Those fields may be empty for simple use cases,
/// > or they may contain valuable information related to the movement of tokens, similar to
/// > information attached to a bank transfer by the sender or the bank itself.
/// > The use of a data field is equally present in other standard proposals such as EIP-223, and
/// > was requested by multiple members of the community who reviewed this standard.
/// >
/// - https://eips.ethereum.org/EIPS/eip-777#data
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct TransferCallData(pub Vec<u8>);
impl Deref for TransferCallData {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
} |
Regarding renaming to |
done |
REJECTED: Change Proposal: drop
|
REJECTED: Change Proposal: simplify
|
|
Dropping |
I see ... I misinterpreted the semantics for To clarify, you mean:
In that case I agree with you, but I will propose the following minor changes:
|
I am going based on the assumption that the receiver account must be pre-registered with the FT contract. Thus, that should prevent this class of human mistakes because the receiver contract must support the tokens since they must actively pre-register. Or is there another type of human mistake you are thinking of.
I guess that depends on the purpose of the If we want to support refunds and confirmations, then the transferred funds should be locked - either via vault based approach (#122) or lock based mechanism used in (#110) for transfer confirmations. What are your thoughts on adding transfer functions for vault based transfer and transfer confirmation: /// tokens are locked until the receiver contract confirms the transfer
fn ft_confirm_transfer(
&mut self,
recipient: ValidAccountId,
amount: TokenAmount,
memo: Option<String>,
data: Option<String>,
) -> Promise;
fn ft_transfer_with_vault(
&mut self,
recipient: ValidAccountId,
amount: TokenAmount,
memo: Option<String>,
data: Option<String>,
) -> Promise; Having separate transfer workflows provides more flexibility and applications can choose the transfer mechanism that best fits their use case.
Can you please elaborate on this. How much extra? |
Pre-registration does indeed guards against unwanted usage, but it doesn't block someone from registering an unsupported contract.
If the receiver contract is required to return the used
I don't think |
Being able to return the amount of tokens received doesn't complicate the receiver's contract implementation. It still transactional, since with NEAR async environment, the receiver's contract relies on the inner balance value, instead of using |
@evgenykuzyakov - thanks that clears things up for me and convinces me to change my vote - I am going to close out that change proposal. But what are your thoughts on |
Shoudn't |
makes sense - data should be serialized as efficiently as possible in order to scale efficiently ... recommending Borsh serialization as the preferred binary transport format |
Change Proposal: Replace primitive types with domain specific wrappers has been updated with 2 more domain types:
|
@miohtama has made me rethink whether or not transfer call data should be optional ... data may not be required for simple use cases. To make the simple use case as efficient as possible, then call data should be made optional. If the receiver requires call data to process the transfer, then it should fail the transfer if the call data is not specified or not valid. @evgenykuzyakov cited the following benefit for making call data required
How does making the call data required add value as an extra guard on the receiver side? The receiver side is already guarded by the following:
What percentage of transfer use cases will require call data? I would be convinced to make it required if more than 90% of the transfer call use cases will require call data. Otherwise, let's make it optional. |
REJECTED: Change Proposal: Add contract function to burn tokensREJECT REASON: this belongs in separate standard as an optional extension pub trait FungibleTokenCore {
/// Destroys specified amount of tokens from the predecessor account, reducing the total supply.
///
/// # Panics
/// - if there is no attached deposit
/// - if account is not registered
/// - if amount is zero
/// - if the account has insufficient funds to fulfill the request
///
/// #\[payable\]
fn ft_burn(&mut self, amount: TokenAmount, memo: Option<Memo>);
// ...
} |
What should be the goals and principles of the core FT standard? Here are my thoughts:
Are there any goals or criteria that I am missing? Using the above guidelines and criteria, helps me to focus my thoughts to better decide what should be in scope for the core FT standard. Without clear goals and criteria, the discussion will tend to drift and bloat. I am guilty of it ... For example, when applying these guidelines it tells me that
That being said, both Examples of applying the above principles and guidelines:
To bring this full circle, I believe we are pretty close (if not there already) at arriving at the core FT standard: use near_sdk::{
borsh::{self, BorshDeserialize, BorshSerialize},
json_types::{ValidAccountId, U128},
serde::{Deserialize, Serialize},
Promise, PromiseOrValue,
};
use std::{
cmp::Ordering,
fmt::{self, Display, Formatter},
ops::{Deref, DerefMut},
};
/// Defines the standard interface for the core Fungible Token contract
/// - [NEP-141](https://github.com/near/NEPs/issues/141)
///
/// The core standard supports the following features:
/// - [simple token transfers](FungibleTokenCore::ft_transfer)
/// - [token transfers between contracts](FungibleTokenCore::ft_transfer_call)
/// - [burning tokens](FungibleTokenCore::ft_burn)
/// - accounting for [total token supply](FungibleTokenCore::ft_total_supply) and
/// [account balances](FungibleTokenCore::ft_balance_of)
///
/// ## Notes
/// - it doesn't include token metadata standard that will be covered by a separate NEP, because the
/// metadata may evolve.
/// - it also doesn't include account registration standard that also should be covered by a separate
/// NEP because it can be reused for other contract.
///
/// ### Security
/// Requirement for accept attached deposits (#\[payable\])
/// Due to the nature of function-call permission access keys on NEAR protocol, the method that
/// requires an attached deposit can't be called by the restricted access key. If the token contract
/// requires an attached deposit of at least 1 yoctoNEAR on transfer methods, then the function-call
/// restricted access key will not be able to call them without going through the wallet confirmation.
/// This prevents some attacks like fishing through an authorization to a token contract.
///
/// This 1 yoctoNEAR is not enforced by this standard, but is encouraged to do. While ability to
/// receive attached deposit is enforced by this token.
///
/// ### Transfer Call Refunds
/// If the receiver contract is malicious or incorrectly implemented, then the receiver's promise
/// result may be invalid and the required balance may not be available on the receiver's account.
/// In this case the refund can't be provided provided to the sender. This is prevented by #122
/// standard that locks funds into a temporary vault and prevents receiver from overspending the
/// funds and later retuning invalid value. But if this flaw exist in this standard, it's not an
/// issue for the sender account. It only affects the transfer amount and the receiver's account
/// balance. The receiver can't overspend tokens from the sender outside of sent amount, so this
/// standard should be considered as safe as #122
///
pub trait FungibleTokenCore {
/// Enables simple transfer between accounts.
///
/// - Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id`.
/// - Both accounts should be registered with the contract for transfer to succeed.
/// - Method is required to be able to accept attached deposits - to not panic on attached deposit.
/// See security section of the standard.
///
/// Arguments:
/// - `receiver_id` - the account ID of the receiver.
/// - `amount` - the amount of tokens to transfer. Should be a positive number in decimal string representation.
/// - `memo` - an optional string field in a free form to associate a memo with this transfer.
///
/// ## Panics
/// - if there is no attached deposit
/// - if either sender or receiver accounts are not registered
/// - if amount is zero
/// - if the sender account has insufficient funds to fulfill the request
///
/// #\[payable\]
fn ft_transfer(&mut self, receiver_id: ValidAccountId, amount: TokenAmount, memo: Option<Memo>);
/// Transfer to a contract with a callback.
///
/// Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id`
/// account. Then calls [`FungibleTokenReceiver::ft_on_transfer`] method on `receiver_id` contract
/// and attaches a callback to resolve this transfer.
/// [`FungibleTokenReceiver::ft_on_transfer`] method should return the amount of tokens used by
/// the receiver contract, the remaining tokens should be refunded to the `predecessor_account_id`
/// at the resolve transfer callback.
///
/// Token contract should pass all the remaining unused gas to [`FungibleTokenReceiver::ft_on_transfer`]
///
/// Malicious or invalid behavior by the receiver's contract:
/// - If the receiver contract promise fails or returns invalid value, the full transfer amount
/// should be refunded.
/// - If the receiver contract overspent the tokens, and the `receiver_id` balance is lower
/// than the required refund amount, the remaining balance should be refunded.
///
/// Both accounts should be registered with the contract for transfer to succeed.
/// Method is required to be able to accept attached deposits - to not panic on attached deposit. See Security
/// section of the standard.
///
/// Arguments:
/// - `receiver_id` - the account ID of the receiver contract. This contract will be called.
/// - `amount` - the amount of tokens to transfer. Should be a positive number in decimal string representation.
/// - `data` - a string message that will be passed to `ft_on_transfer` contract call.
/// - `memo` - an optional string field in a free form to associate a memo with this transfer.
/// Returns a promise to resolve transfer call which will return the used amount (see suggested trait to resolve
/// transfer).
///
/// ## Panics
/// - if there is no attached deposit
/// - if either sender or receiver accounts are not registered
/// - if amount is zero
/// - if the sender account has insufficient funds to fulfill the transfer request
///
/// #\[payable\]
fn ft_transfer_call(
&mut self,
receiver_id: ValidAccountId,
amount: TokenAmount,
data: Option<TransferCallData>,
memo: Option<Memo>,
) -> Promise;
/// Destroys specified amount of tokens from the predecessor account, reducing the total supply.
///
/// # Panics
/// - if there is no attached deposit
/// - if account is not registered
/// - if amount is zero
/// - if the account has insufficient funds to fulfill the transfer request
///
/// #\[payable\]
fn ft_burn(&mut self, amount: TokenAmount, memo: Option<Memo>);
fn ft_total_supply(&self) -> TokenAmount;
fn ft_balance_of(&self, account_id: ValidAccountId) -> TokenAmount;
}
/// Receiver of the Fungible Token for [`FungibleTokenCore::ft_transfer_call`] calls.
pub trait FungibleTokenReceiver {
/// Callback to receive tokens.
///
/// Called by fungible token contract `env::predecessor_account_id` after `transfer_call` was initiated by
/// `sender_id` of the given `amount` with the transfer message given in `msg` field.
/// The `amount` of tokens were already transferred to this contract account and ready to be used.
///
/// The method should return the amount of tokens that are used/accepted by this contract from the transferred
/// amount. Examples:
/// - The transferred amount was `500`, the contract completely takes it and should return `500`.
/// - The transferred amount was `500`, but this transfer call only needs `450` for the action passed in the `msg`
/// field, then the method should return `450`.
/// - The transferred amount was `500`, but the action in `msg` field has expired and the transfer should be
/// cancelled. The method should return `0` or panic.
///
/// Arguments:
/// - `sender_id` - the account ID that initiated the transfer.
/// - `amount` - the amount of tokens that were transferred to this account.
/// - `msg` - a string message that was passed with this transfer call.
///
/// Returns the amount of tokens that are used/accepted by this contract from the transferred amount.
fn ft_on_transfer(
&mut self,
sender_id: ValidAccountId,
amount: TokenAmount,
msg: Option<TransferCallData>,
) -> PromiseOrValue<TokenAmount>;
}
/// Suggested Trait to handle the callback on fungible token contract to resolve transfer.
/// It's not a public interface, so fungible token contract can implement it differently.
pub trait FungibleTokenCoreResolveTransferCall {
/// Callback to resolve transfer.
/// Private method (`env::predecessor_account_id == env::current_account_id`).
///
/// Called after the receiver handles the transfer call and returns value of used amount in `U128`.
///
/// This method should get `used_amount` from the receiver's promise result and refund the remaining
/// `amount - used_amount` from the receiver's account back to the `sender_id` account.
/// Methods returns the amount tokens that were spent from `sender_id` after the refund
/// (`amount - min(receiver_balance, used_amount)`)
///
/// Arguments:
/// - `sender_id` - the account ID that initiated the transfer.
/// - `receiver_id` - the account ID of the receiver contract.
/// - `amount` - the amount of tokens that were transferred to receiver's account.
///
/// Promise results:
/// - `used_amount` - the amount of tokens that were used by receiver's contract. Received from `on_ft_receive`.
/// `used_amount` should be `U128` in range from `0` to `amount`. All other invalid values are considered to be
/// equal to `0`.
///
/// Returns the amount of tokens that were spent from the `sender_id` account. Note, this value might be different
/// from the `used_amount` returned by the receiver contract, in case the refunded balance is not available on the
/// receiver's account.
///
/// #\[private\]
fn resolve_transfer_call(
&mut self,
sender_id: ValidAccountId,
receiver_id: ValidAccountId,
amount: TokenAmount,
// NOTE: #[callback_result] is not supported yet and has to be handled using lower level interface.
//
// #[callback_result]
// used_amount: CallbackResult<TokenAmount>,
);
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct TokenAmount(pub U128);
impl From<u128> for TokenAmount {
fn from(value: u128) -> Self {
Self(value.into())
}
}
impl TokenAmount {
pub fn value(&self) -> u128 {
self.0 .0
}
}
impl Deref for TokenAmount {
type Target = u128;
fn deref(&self) -> &Self::Target {
&self.0 .0
}
}
impl DerefMut for TokenAmount {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 .0
}
}
impl Display for TokenAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0 .0.fmt(f)
}
}
impl PartialOrd for TokenAmount {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value().partial_cmp(&other.value())
}
}
impl Ord for TokenAmount {
fn cmp(&self, other: &Self) -> Ordering {
self.value().cmp(&other.value())
}
}
impl Eq for TokenAmount {}
/// > 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.
///
/// - https://github.com/near/NEPs/issues/136
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct Memo(pub String);
impl Deref for Memo {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Memo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// > The mint, send and burn processes can all make use of a data and operatorData fields which are
/// > passed to any movement (mint, send or burn). Those fields may be empty for simple use cases,
/// > or they may contain valuable information related to the movement of tokens, similar to
/// > information attached to a bank transfer by the sender or the bank itself.
/// > The use of a data field is equally present in other standard proposals such as EIP-223, and
/// > was requested by multiple members of the community who reviewed this standard.
/// >
/// - https://eips.ethereum.org/EIPS/eip-777#data
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct TransferCallData(pub Vec<u8>);
impl Deref for TransferCallData {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
} NOTESEvents should be defined as part of the standard. However, because NEAR does not currently support events, events have been intentionally left out of scope. Once NEAR officially supports events, then events should be reconsidered. |
Considering JSON serializes Explaining
|
@evgenykuzyakov - thanks for fully explaining use near_sdk::{
borsh::{self, BorshDeserialize, BorshSerialize},
json_types::{ValidAccountId, U128},
serde::{Deserialize, Serialize},
Promise, PromiseOrValue,
};
use std::{
cmp::Ordering,
fmt::{self, Display, Formatter},
ops::{Deref, DerefMut},
};
/// Defines the standard interface for the core Fungible Token contract
/// - [NEP-141](https://github.com/near/NEPs/issues/141)
///
/// The core standard supports the following features:
/// - [simple token transfers](FungibleTokenCore::ft_transfer)
/// - [token transfers between contracts](FungibleTokenCore::ft_transfer_call)
/// - accounting for [total token supply](FungibleTokenCore::ft_total_supply) and
/// [account balances](FungibleTokenCore::ft_balance_of)
///
/// ## Notes
/// - it doesn't include token metadata standard that will be covered by a separate NEP, because the
/// metadata may evolve.
/// - it also doesn't include account registration standard that also should be covered by a separate
/// NEP because it can be reused for other contract.
///
/// ### Security
/// Requirement for accept attached deposits (#\[payable\])
/// Due to the nature of function-call permission access keys on NEAR protocol, the method that
/// requires an attached deposit can't be called by the restricted access key. If the token contract
/// requires an attached deposit of at least 1 yoctoNEAR on transfer methods, then the function-call
/// restricted access key will not be able to call them without going through the wallet confirmation.
/// This prevents some attacks like fishing through an authorization to a token contract.
///
/// This 1 yoctoNEAR is not enforced by this standard, but is encouraged to do. While ability to
/// receive attached deposit is enforced by this token.
///
/// ### Transfer Call Refunds
/// If the receiver contract is malicious or incorrectly implemented, then the receiver's promise
/// result may be invalid and the required balance may not be available on the receiver's account.
/// In this case the refund can't be provided provided to the sender. This is prevented by #122
/// standard that locks funds into a temporary vault and prevents receiver from overspending the
/// funds and later retuning invalid value. But if this flaw exist in this standard, it's not an
/// issue for the sender account. It only affects the transfer amount and the receiver's account
/// balance. The receiver can't overspend tokens from the sender outside of sent amount, so this
/// standard should be considered as safe as #122
///
pub trait FungibleTokenCore {
/// Enables simple transfer between accounts.
///
/// - Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id`.
/// - Both accounts should be registered with the contract for transfer to succeed.
/// - Method is required to be able to accept attached deposits - to not panic on attached deposit.
/// See security section of the standard.
///
/// Arguments:
/// - `receiver_id` - the account ID of the receiver.
/// - `amount` - the amount of tokens to transfer. Should be a positive number in decimal string representation.
/// - `memo` - an optional string field in a free form to associate a memo with this transfer.
///
/// ## Panics
/// - if there is no attached deposit
/// - if either sender or receiver accounts are not registered
/// - if amount is zero
/// - if the sender account has insufficient funds to fulfill the request
///
/// #\[payable\]
fn ft_transfer(&mut self, receiver_id: ValidAccountId, amount: TokenAmount, memo: Option<Memo>);
/// Transfer to a contract with a callback.
///
/// Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id`
/// account. Then calls [`FungibleTokenReceiver::ft_on_transfer`] method on `receiver_id` contract
/// and attaches a callback to resolve this transfer.
/// [`FungibleTokenReceiver::ft_on_transfer`] method should return the amount of tokens used by
/// the receiver contract, the remaining tokens should be refunded to the `predecessor_account_id`
/// at the resolve transfer callback.
///
/// Token contract should pass all the remaining unused gas to [`FungibleTokenReceiver::ft_on_transfer`]
///
/// Malicious or invalid behavior by the receiver's contract:
/// - If the receiver contract promise fails or returns invalid value, the full transfer amount
/// should be refunded.
/// - If the receiver contract overspent the tokens, and the `receiver_id` balance is lower
/// than the required refund amount, the remaining balance should be refunded.
///
/// Both accounts should be registered with the contract for transfer to succeed.
/// Method is required to be able to accept attached deposits - to not panic on attached deposit. See Security
/// section of the standard.
///
/// Arguments:
/// - `receiver_id` - the account ID of the receiver contract. This contract will be called.
/// - `amount` - the amount of tokens to transfer. Should be a positive number in decimal string representation.
/// - `data` - a string message that will be passed to `ft_on_transfer` contract call.
/// - `memo` - an optional string field in a free form to associate a memo with this transfer.
/// Returns a promise to resolve transfer call which will return the used amount (see suggested trait to resolve
/// transfer).
///
/// ## Panics
/// - if there is no attached deposit
/// - if either sender or receiver accounts are not registered
/// - if amount is zero
/// - if the sender account has insufficient funds to fulfill the transfer request
///
/// #\[payable\]
fn ft_transfer_call(
&mut self,
receiver_id: ValidAccountId,
amount: TokenAmount,
data: TransferCallData,
memo: Option<Memo>,
) -> Promise;
fn ft_total_supply(&self) -> TokenAmount;
fn ft_balance_of(&self, account_id: ValidAccountId) -> TokenAmount;
}
/// Receiver of the Fungible Token for [`FungibleTokenCore::ft_transfer_call`] calls.
pub trait FungibleTokenReceiver {
/// Callback to receive tokens.
///
/// Called by fungible token contract `env::predecessor_account_id` after `transfer_call` was initiated by
/// `sender_id` of the given `amount` with the transfer message given in `msg` field.
/// The `amount` of tokens were already transferred to this contract account and ready to be used.
///
/// The method should return the amount of tokens that are used/accepted by this contract from the transferred
/// amount. Examples:
/// - The transferred amount was `500`, the contract completely takes it and should return `500`.
/// - The transferred amount was `500`, but this transfer call only needs `450` for the action passed in the `msg`
/// field, then the method should return `450`.
/// - The transferred amount was `500`, but the action in `msg` field has expired and the transfer should be
/// cancelled. The method should return `0` or panic.
///
/// Arguments:
/// - `sender_id` - the account ID that initiated the transfer.
/// - `amount` - the amount of tokens that were transferred to this account.
/// - `msg` - a string message that was passed with this transfer call.
///
/// Returns the amount of tokens that are used/accepted by this contract from the transferred amount.
fn ft_on_transfer(
&mut self,
sender_id: ValidAccountId,
amount: TokenAmount,
data: TransferCallData,
) -> PromiseOrValue<TokenAmount>;
}
/// Suggested Trait to handle the callback on fungible token contract to resolve transfer.
/// It's not a public interface, so fungible token contract can implement it differently.
pub trait FungibleTokenCoreResolveTransferCall {
/// Callback to resolve transfer.
/// Private method (`env::predecessor_account_id == env::current_account_id`).
///
/// Called after the receiver handles the transfer call and returns value of used amount in `U128`.
///
/// This method should get `used_amount` from the receiver's promise result and refund the remaining
/// `amount - used_amount` from the receiver's account back to the `sender_id` account.
/// Methods returns the amount tokens that were spent from `sender_id` after the refund
/// (`amount - min(receiver_balance, used_amount)`)
///
/// Arguments:
/// - `sender_id` - the account ID that initiated the transfer.
/// - `receiver_id` - the account ID of the receiver contract.
/// - `amount` - the amount of tokens that were transferred to receiver's account.
///
/// Promise results:
/// - `used_amount` - the amount of tokens that were used by receiver's contract. Received from `on_ft_receive`.
/// `used_amount` should be `U128` in range from `0` to `amount`. All other invalid values are considered to be
/// equal to `0`.
///
/// Returns the amount of tokens that were spent from the `sender_id` account. Note, this value might be different
/// from the `used_amount` returned by the receiver contract, in case the refunded balance is not available on the
/// receiver's account.
///
/// #\[private\]
fn resolve_transfer_call(
&mut self,
sender_id: ValidAccountId,
receiver_id: ValidAccountId,
amount: TokenAmount,
// NOTE: #[callback_result] is not supported yet and has to be handled using lower level interface.
//
// #[callback_result]
// used_amount: CallbackResult<TokenAmount>,
);
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct TokenAmount(pub U128);
impl From<u128> for TokenAmount {
fn from(value: u128) -> Self {
Self(value.into())
}
}
impl TokenAmount {
pub fn value(&self) -> u128 {
self.0 .0
}
}
impl Deref for TokenAmount {
type Target = u128;
fn deref(&self) -> &Self::Target {
&self.0 .0
}
}
impl DerefMut for TokenAmount {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 .0
}
}
impl Display for TokenAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0 .0.fmt(f)
}
}
impl PartialOrd for TokenAmount {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value().partial_cmp(&other.value())
}
}
impl Ord for TokenAmount {
fn cmp(&self, other: &Self) -> Ordering {
self.value().cmp(&other.value())
}
}
impl Eq for TokenAmount {}
/// > 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.
///
/// - https://github.com/near/NEPs/issues/136
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct Memo(pub String);
impl Deref for Memo {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Memo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// > The mint, send and burn processes can all make use of a data and operatorData fields which are
/// > passed to any movement (mint, send or burn). Those fields may be empty for simple use cases,
/// > or they may contain valuable information related to the movement of tokens, similar to
/// > information attached to a bank transfer by the sender or the bank itself.
/// > The use of a data field is equally present in other standard proposals such as EIP-223, and
/// > was requested by multiple members of the community who reviewed this standard.
/// >
/// - https://eips.ethereum.org/EIPS/eip-777#data
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct TransferCallData(pub String);
impl Deref for TransferCallData {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for TransferCallData {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
} |
My 2 cents. To make it simpler to read and understand, let's remove the strongly domain typed params.
Then let's return void from the call, and just check if it was promise_success or reverse the transfer. That's the simplest mechanism. |
It's not supported as a high-level API, but supported on slightly lower-level using |
the type system and compiler is your friend ... the more information you can provide the compiler, the more you can leverage and benefit from the compiler to help you to produce robust and high quality code. This is especially true and needed when writing smart contracts - IMHO. In addition, with Rust's zero-cost abstractions, you can model your domain in the code, but pay zero cost at runtime. One other thing to note is that the Rust contract interface is decoupled from the JSON RPC interface. For example, both snippets of code map to the same JSON RPC API fn ft_transfer_call(
&mut self,
receiver_id: ValidAccountId,
amount: TokenAmount,
data: TransferCallData,
memo: Option<Memo>,
) -> Promise; fn ft_transfer_call(
&mut self,
receiver_id: String,
amount: String, // near_sdk provides a U128 wrapper, but on the wire its a string to work around JavaScript
data: String,
memo: Option<String>,
) -> Promise; Personally, I rather choose to work with the strongly typed interface when writing the Rust contract - but you are free to implement the same contract interface how you wish. |
I'm against dropping
|
I'm not convinced by this. Are we going to use prefixes for every smart-contract standard? This is not a Rust style, nor TypeScript nor AssemblyScript. We don't need to have namespaces in function names. |
@oysterpack There is no race condition in |
Data is even more ambiguous. We already had a chat about it, and I will strongly defend Also, re custom types (
|
I agree with all @evgenykuzyakov comments.
|
The reason for locking Whatever this {"receiver_id": "alice", "amount": "100", "msg": "bXNzZw=="} |
@evgenykuzyakov Got it. I was looking the earlier discussion, got confused. |
It's not supported yet as a high-level API, I would recommend to not include it in this standard. It will discourage newcomers. We should return void from the call, and just check if it was promise_success or reverse the transfer. That's the simplest mechanism. The rare use cases where the receiving contract does not keeps all the transferred tokens can be solved by the receiving contract calling ft_transfer to return the extra tokens. pub trait FungibleTokenCore {
#[payable]
fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option<String>);
#[payable]
fn ft_transfer_call(
&mut self,
receiver_id: AccountId,
amount: U128,
msg: String,
memo: Option<String>,
) -> Promise;
fn ft_total_supply(&self) -> U128;
fn ft_balance_of(&self, account_id: AccountId) -> U128;
}
pub trait FungibleTokenReceiver {
fn ft_on_transfer(
&mut self,
sender_id: AccountId,
amount: U128,
msg: String,
memo: Option<String>
) ;
}
pub trait FungibleTokenCore {
#[private]
//this is executed after ReceivingContract.ft_on_transfer()...
fn ft_resolve_transfer(
&mut self,
sender_id: AccountId,
receiver_id: AccountId,
amount: U128
);
// undo the transfer if the receiving contract did not execute correctly
// { if !env::is_promise_success() { self.internal_transfer(receiver_id, sender_id, amount.0 } }
} |
When checking Also we plan to add |
Ok, I understand, it is required to code slippage more efficiently in something like Uniswap V2 on NEAR native |
thanks @mikedotexe for organizing the "FT Standard Discussion" meeting tomorrow. I propose we use the JSON API as the baseline to organize the discussion around. It's been a great discussion and it would be good to summarize where people stand going into the meeting.
|
Proposal: Invert the returned value from the The method MUST return the amount of unused tokens. Examples: The transferred amount was 500, the contract completely takes it and MUST return 0. Arguments: sender_id - the account ID that initiated the transfer. |
Let's change the language.
|
exactly 1 yoctoNEAR must be attached to |
@nearmax raised some concerned about JSON serialization: https://gov.near.org/t/evm-runtime-base-token/340/24 So I just wanted to make sure it's decided that all arguments in the standard are going to be serialized in JSON? |
@ilblackdragon I responded in the gov.near.org. I don't know what Node is using for wallet / dapp API - I don't think this is a problem. In this standard, there are no complex arguments - all of them are only strings which doesn't require a complex object. So it's a string representing a number, or a string representing some not defined bytes (eg base64), or just a string (message, account ,...). Developer may decide to use JSON to encode a complex object in |
@robert-zaremba it's not a big deal indeed in the case of this standard, but protocol ideally should be very well defined on the byte level. For example are any of these valid parsed arguments:
There are plenty of weird JSON stuff: https://github.com/c9fe/weird-json |
Wrapping out and putting things togetherI'm wrapping all this out in : https://github.com/defi-guild/fungible-token IMPORTANT:The order of arguments in |
I have a few points:
|
Hey @robert-zaremba you also removed all @evgenykuzyakov 's code comments in the functions. I think the comments help a lot and should be kept. It was:
and here https://github.com/defi-guild/fungible-token the comments are removed:
|
Playing around with the NEP-141 standard in our current code base. Problem I'm running into is that I would prefer for users not to have to "pre-pay" storage. I think the standard still holds true but it would be better if in the The implementation would be more similar to this:
I think the pre-paid model is a good fallback model but that it should be optional since it does require an extra step and will throw users / frontend devs off once storage runs out. Maybe this would cause the same issue as we're trying to solve before where a user's deposit would get "stuck" in the token contract if the promise fails but I think this could be handled in |
@luciotato - we will add comments in the detailed documentation, I just wanted to have a quick and short overview. As for optional - to be hones I think both of them should be |
NOTE: this NEP is closed and the new process is to use discussions (for discussions)
|
can confuse newcomers. Eugene could also update what's at http://github.com/near/neps/pull/141 to reflect the new standard
Let's add a link in the top of this issue description linking to the NEP-141 discussion. cc @evgenykuzyakov |
Fungible Token Core Standard
Background
This standard is a subset of the proposed #136 that only covers the core contract API.
Please read the linked standard.
It doesn't include token metadata standard that will be covered by a separate NEP, because the metadata may evolve.
It also doesn't include account registration standard that also should be covered by a separate NEP because it can be reused for other contract.
It's also not exact core API from the #136
This standard allows to specify the amount of tokens used by the contract and refund a part. It also renames some arguments to
be more consistent with the original fungible token standard #21
See #122 (comment) for the rationale to move from #122
Proposal
API
Token contract should implement the following Trait.
The receiver contract should implement the following Trait
Suggested Trait to handle the callback on fungible token contract to resolve transfer. It's not a public interface, so
fungible token contract can implement it differently.
Notes
memo
field is explained in details in #136The goal is to associate some external information with the transfer on chain. It may be needed for compliance in some
jurisdictions.
Security
Requirement for accept attached deposits (
#[payable]
)Due to the nature of function-call permission access keys on NEAR protocol, the method that requires an attached deposit
can't be called by the restricted access key. If the token contract requires an attached deposit of at least 1
yoctoNEAR on transfer methods, then the function-call restricted access key will not be able to call them without going
through the wallet confirmation. This prevents some attacks like fishing through an authorization to a token contract.
This 1 yoctoNEAR is not enforced by this standard, but is encouraged to do. While ability to receive attached deposit
is enforced by this token.
Transfer Call Refunds
If the receiver contract is malicious or incorrectly implemented, then the receiver's promise result may be invalid and
the required balance may not be available on the receiver's account. In this case the refund can't be provided provided
to the sender. This is prevented by #122 standard that locks funds into a temporary
vault and prevents receiver from overspending the funds and later retuning invalid value.
But if this flaw exist in this standard, it's not an issue for the sender account. It only affects the transfer amount
and the receiver's account balance. The receiver can't overspend tokens from the sender outside of sent amount, so this
standard should be considered as safe as #122
The text was updated successfully, but these errors were encountered: