-
Notifications
You must be signed in to change notification settings - Fork 136
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
Proposal: Allowance-free vault-based token standard #122
Comments
Here I will drop some comments in random order. on_receive_with_safe(sender_id: ValidAccountId, amount: U128, safe_id: SafeId, payload: String); I suggest having The following is only maybe good idea: A lot of smart contract use cases need to know the current balance ( |
I suggest trying to simplify so that there is only one entry-point for transfers:
Do not make two similar functions, as the end-users still may end up to use the wrong one. Here is an example of an end-user executing ERC-20 https://etherscan.io/tx/0x18ff0886581735dc189bdd96a77f01b43a03fe769055fe25f78f3c7d2c1e5502 In this case, the contract or the user cannot recover from this situation. Here are some different use cases we need to separate out
Ideally they all would use the same call signature, and behind the scenes the token smart contract would construct the promise in such a way that Alternatively if the above is not possible offer a standardized function to recover from the situation when the wrong |
A good token standard needs some additional features outside the core transfer semantics
For 1) it is just coming up with some core metadata fields besides For 2) I have no idea how it is supposed to work on NEAR: Is there are a centralised (yuc) indexer server that all clients rely on to get this kind of data? For 3) is now needed more in DeFi scenarios than back in the day when tokens started. Ethereum mints and burns are somwhat fragmented, so it is hard for blockchain explorers like Etherscan to parse when new tokens popped in and out of existence. This leads to the fact that often "the total available supply" number is not correctly tracked. |
Regarding |
Are promises going to be serialised as JSON forever? Isn't that quite inefficient. |
Yep. It seems we're not going back to Borsh anytime soon. So I can assume it'll be json forever :) |
@evgenykuzyakov would you consider also adding a secondary option transfer_to_contract(receiver, amount, payload) , so contract developers can have one more option and use the most convenient according to what's required for the particular interaction? transfer_to_contract sequence:
Receiving side interface
|
I think the issue with this solution is to be able to spend transferred tokens while they are in-flight. E.g. a contract may be able to transfer them all out and then fail (in multiple promises). The token contract will not be able to cancel the transfer. |
The counter-argument is that normally receiving contracts have a limit to spend tokens based on its internal accounting. So it can't spend the funds in-flight because they're not registered in its internal accounting. |
If feel like Also you may need to do a refund. Consider uniswap with slippage limit. Let's say you want to swap 1000 DAI for 1000 USDT. You send 1001 DAI to account for potential slippage of price and uniswap contract has to refund you for the unused DAI amount. |
This comment makes no sense. You are transferring tokens to a contract, so you are already trusting it. |
Maybe I didn't express that correctly, I'll edit for clarity |
FYI. I'm going to update this standard method names and topics to use |
In fact, the |
I'm trying wrap my head around gas sensitivity when there are multiple cross contract calls. What if the |
The funds will not get stuck because the |
The proposed spec has evolved based on @evgenykuzyakov latest and greatest "reference implementation" : My feedback on the latest and greatest is:
Registered Accounts
Thoughts on "How do we finalize the standard ?"... this is a great discussion but we need to bring this to closure soon in order to drive adoption .... To move forward, I propose having live online discussions (via Zoom or other venue) and design sessions to hammer this out. Can someone from the NEAR DEV team please drive this and set this up? I vote for @evgenykuzyakov :)
... closing thoughts ... we need to work on standardizing the process to standardize |
@oysterpack - I agree - we need to finalize standards to drive the adoption. And there is a need for "reactive" fungible tokens. |
Here's my two cents ... all of the proposed standards have different token "transfer" use cases. We need to decouple the transfer protocol from the Fungible Token. I whipped together some quick and dirty interfaces to illustrate the concept. Account registration is required, especially on NEAR because of storage usage fees. The benefits for registered accounts are:
The key to decoupling the transfer protocol interface is to let the registered account choose the transfer protocol from the receiver side. Sender's simply want to transfer tokens to the receiver, unaware of the underlying transfer protocol. The tricky part is gas, because different protocols will have different gas profiles and requirements. Thus, the gas requirements need to be specified as part of the spec on the transfer protocol. Thoughts? use near_sdk::json_types::{ValidAccountId, U128};
use near_sdk::{
ext_contract,
serde::{Deserialize, Serialize},
AccountId, Promise, PromiseOrValue,
};
/// The design intent is to decouple the token asset from the token transfer protocol.
///
/// - Fungible token supports 1 or more [TransferProtocol]s as specified per [MetaData]
/// - Accounts must register with the token contract and pay for account storage fees.
/// - account storage fees are escrowed and refunded when the account unregisters
/// - account chooses the transfer protocol to use as transfer recipient
/// - FT has generic [transfer] function interface
/// - sender account does not choose the transfer protocol - the receiver account chooses how they
/// want to receive the tokens
///
/// The key advantage of this design is that it decouples the protocol interface from the implementation.
/// The problem with all of the "standard" interfaces is that they are too tightly coupled with implementation.
/// We need decoupled interface that will allow transfer protocols to evolve.
pub trait FungibleToken: AccountRegistry {
fn metadata() -> Metadata;
/// Returns total supply.
/// MUST equal to total_amount_of_token_minted - total_amount_of_token_burned
fn total_supply(&self) -> U128;
/// Returns the token balance for `holder` account
fn balance_of(&self, account_id: ValidAccountId) -> U128;
/// ## Panics
/// - if accounts are not registered
/// - insufficient funds
fn transfer(
&mut self,
receiver_id: ValidAccountId,
amount: U128,
msg: Option<String>,
memo: Option<String>,
) -> PromiseOrValue<TransferProtocol>;
}
/// Suggested protocol names:
/// - NEP_122
/// - NEP_136
/// - NEP_21
///
/// - each protocol defines min amount of gas required excluding gas required to cover `msg` `memo`
pub struct TransferProtocol(String, Gas);
pub struct Gas(pub u64);
pub trait AccountRegistry {
/// Registers the predecessor account ID with the contract.
/// The account is required to pay for its storage. Storage fees will be escrowed and refunded
/// when the account is unregistered.
///
/// #[payable]
/// - storage escrow fee is required
/// - use [account_storage_escrow_fee] to lookup the required storage fee amount
/// - any amount above the storage fee will be refunded
///
/// ## Panics
/// - if deposit is not enough to cover storage fees
/// - is account is already registered
/// - if transfer protocol is not supported
///
/// #[payable]
fn register_account(&mut self, transfer_protocol: TransferProtocol);
/// Unregisters the account and refunds the escrowed storage fees.
///
/// ## Panics
/// - if account is not registered
/// - if registered account has funds
fn unregister_account(&mut self);
/// changes the account's transfer type
///
/// ## Panics
/// - if account is not registered
/// - if transfer protocol is not supported
fn set_transfer_type(&mut self, transfer_protocol: TransferProtocol);
/// Burns owned token funds unregisters the account. Escrowed storage fees are refunded.
///
/// ## Panics
/// - if account is not registered
fn burn_account(&mut self);
////////////////////////////
/// VIEW METHODS ///
//////////////////////////
/// Returns the required deposit amount that is required for account registration.
fn account_storage_fee(&self) -> U128;
fn account_registered(&self, account_id: ValidAccountId) -> bool;
/// returns None if the account is not registered
fn account_transfer_protocol(&self, account_id: ValidAccountId) -> Option<String>;
fn total_registered_accounts(&self) -> U128;
}
/// Each token must have 18 digits precision (decimals)
pub const DECIMALS: u8 = 18;
pub struct Metadata {
pub name: String,
pub symbol: String,
/// URL to additional resources about the token.
pub reference: String,
/// the smallest part of the token that’s (denominated in e18) not divisible
/// In other words, the granularity is the smallest amount of tokens (in the internal denomination)
/// which MAY be minted, sent or burned at any time.
/// - 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.
///
/// NOTE: Most tokens SHOULD be fully partition-able. I.e., this function SHOULD return 1 unless
/// there is a good reason for not allowing any fraction of the token.
pub granularity: U128,
/// Transfer protocols that are supported by the token contract
pub supported_transfer_protocols: Vec<TransferProtocol>,
} |
To help drive this, we should pull in the NEAR wallet team into the discussion ... the NEAR wallet should/must have built in support for FT and would be an excellent POC to drive the FT interface standardization. If we can make this work for the NEAR wallet, then it would pave the way for community adoption and be a boon for the NEAR ecosystem. |
I agree that the account registration should be a separate standard. Because this pattern is likely will be used for other contracts and standards. As for the fungible token standard, I tried to design the bare minimum that is required for the transfers to work. Metadata is not part of the standard, because it's not needed for the main functionality to work. It's required for wallets to display the token, but it doesn't have to belong to this particular standard. As for the Also, note that this standard lacked |
It this a comment to this proposal, or to #136 ? |
I was writing about a need for |
@oysterpack , your motivation for |
|
But if we standardize events as part of this NEP and one of the event requires to put a memo, then we can include it as required. But even an event may have optional fields. |
@evgenykuzyakov is an empty string ( |
@robert-zaremba I am not saying to have a separate contract for AccountRegistry - I am saying the token contract should implement the AccountRegistry interface in addition to the FungibleToken interface: pub trait FungibleToken: AccountRegistry {
...
} and regarding Option ... it is a best practice because the intent is explicit, i.e., something that is optional should be wrapped in Option |
Ah, I didn't notice the inheritance. |
Required numeric arguments should never be passed in as empty string, and it is better to fail fast with a validation error. The intent should be clear ... let the type system do its job - it makes the code cleaner, more robust, prevents bugs, helps protect against human error, etc That being said, let's say you had reason for a type such as let value: Option<String> = Some("".to_string());
match value {
None => println!("value is None"),
Some(value) if value.is_empty() => println!("value is empty string"),
Some(value) => println!("value is {}", value),
} |
I thought it would be good to share some working code ... I have implemented NEP-122 for the STAKE token project I am working on using @evgenykuzyakov's implementation within Berry Farm as a reference with the following modifications:
CodeVaultFungibleTokenAccountManagementThe contract is deployed on testnet at NEAR CLI Examples# VaultFungibleToken
near view stake.oysterpack.testnet get_total_supply
near view stake.oysterpack.testnet get_balance --args '{"account_id":"alfio-zappala-oysterpack.testnet"}'
near call stake.oysterpack.testnet transfer --accountId alfio-zappala-oysterpack.testnet --args '{"receiver_id":"oysterpack.testnet", "amount":"1000000000000"}'
# AccountManagement
# returns the account storage fee amount that is required when registering an account
near view stake.oysterpack.testnet account_storage_fee
# payment is required for account storage usage fee (currently 0.0681 NEAR) - change is refunded
near call stake.oysterpack.testnet register_account --accountId oysterpack.testnet --amount 1
near call stake.oysterpack.testnet unregister_account --accountId alfio-zappala-oysterpack.testnet NOTE: I haven't tested the Feedback is welcomed and appreciated. |
@oysterpack, yes, this is what I meant. |
@oysterpack - in your use-case, what is the advantage of using vaults vs #136 ? |
@robert-zaremba I would say it depends on the specific use case which is determined by the specific receiver. I chose to implement the vault based implementation simply because I needed to choose something for my STAKE token project the use cases for both
Both require a "finalize" callback on the token contract, in NEP-122 this is defined as fn resolve_vault(&mut self, vault_id: VaultId, sender_id: AccountId) -> U128; and in NEP-136 the proposed "finalize" callback is NEP-122 locks up a "temporary one-time allowance" within the "vault", and it is up to the receiver to withdraw the tokens from the vault. ... this brings me back to my previous comment in the discussion ... There is a use case for both ... and there might be more use cases we think of in the future ... the problem with the current
When the receiver is a contract, it should decide how to receive the tokens. If the token contract supports account registration Go back and take a look at: https://github.com/oysterpack/oysterpack-near-stake-token/blob/main/contract/tests/fungible_token.rs This discussion has been focused on the token transfer protocol, but standardizing metadata and having a token registry are In closing, I leave with you with some food for thought ... How can we bring this FT standard home and to a conclusion ?"the proof is in the pudding" ... we need to drive and prove the standard with live code. I offer my STAKE token project to be used as a POC, but that is only side of the equation. We need to prove is end-to-end. We also need POC contracts that represent receiver accounts. And most importantly, to prove the usability and complete experience we need an FT wallet POC application. The logical best choice that comes to mind is the NEAR wallet. Including standard FT support in the NEAR wallet would be huge and pave the way for an illustrious FT market. This is key to deliver on NEAR's vision:
To help drive this standard and bring it home, it needs leadership and some form of dedicated "task-force". We need to avoid the analysis-paralysis trap ... this is where we need to get folks from the NEAR team more engaged |
@oysterpack I like that. I can use your STAKE code for NEP-136 PoC unless you will like to do it. |
@robert-zaremba since I am actively working on it, I can whip that together pretty fast. I'll let you know once it's deployed on testnet ... regarding NEAR wallet ... FT should be a core NEAR standard ... in order to harness the Internet of value, then the value must be easy to trade and transfer ... the path to fastest adoption is wallet adoption ... thus, regardless, we need to ensure the FT standard makes it simple for wallet integration and the end consumer. Having standard FT support within the NEAR wallet would be a game changer and provide a turbo boost for building dApps ... IMHO |
@robert-zaremba the latest and greatest version of the STAKE token has been deployed to testnet at It implements 3 types of FT transfer protocols:
see the code and it's also included below NOTES
use crate::domain::{self, Gas};
use near_sdk::json_types::{ValidAccountId, U128};
#[allow(unused_imports)]
use near_sdk::AccountId;
use near_sdk::{
borsh::{self, BorshDeserialize, BorshSerialize},
ext_contract,
serde::{Deserialize, Serialize},
Promise,
};
/// - Fungible token supports 1 or more [TransferProtocol]s as specified per [MetaData]
/// - Accounts must register with the token contract and pay for account storage fees.
/// - account storage fees are escrowed and refunded when the account unregisters
/// - account chooses the transfer protocol to use as transfer recipient
pub trait FungibleToken {
fn metadata(&self) -> Metadata;
/// Returns total supply.
/// MUST equal to total_amount_of_token_minted - total_amount_of_token_burned
fn total_supply(&self) -> U128;
/// Returns the token balance for `holder` account
fn balance(&self, account_id: ValidAccountId) -> U128;
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "near_sdk::serde")]
pub struct Metadata {
pub name: String,
pub symbol: String,
/// URL to additional resources about the token.
pub reference: Option<String>,
/// the smallest part of the token that’s (denominated in e18) not divisible
/// In other words, the granularity is the smallest amount of tokens (in the internal denomination)
/// which MAY be minted, sent or burned at any time.
/// - 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.
///
/// NOTE: Most tokens SHOULD be fully partition-able. I.e., this function SHOULD return 1 unless
/// there is a good reason for not allowing any fraction of the token.
pub granularity: u8,
/// Transfer protocols that are supported by the token contract
pub supported_transfer_protocols: Vec<TransferProtocol>,
}
impl Metadata {
/// Each token must have 18 digits precision (decimals)
pub const DECIMALS: u8 = 18;
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "near_sdk::serde")]
pub struct TransferProtocol {
/// Suggested protocol names:
/// - simple - NEP-21
/// - allowance - NEP 21
/// - vault_transfer - NEP-122
/// - transfer_and_notify - NEP-136
pub name: String,
/// - each protocol defines min amount of gas required, excluding gas required to cover `msg` `memo`
pub gas: Gas,
}
impl TransferProtocol {
pub fn simple(gas: Gas) -> Self {
Self {
name: "simple".to_string(),
gas,
}
}
pub fn allowance(gas: Gas) -> Self {
Self {
name: "allowance".to_string(),
gas,
}
}
pub fn vault_transfer(gas: Gas) -> Self {
Self {
name: "vault_transfer".to_string(),
gas,
}
}
pub fn transfer_and_notify(gas: Gas) -> Self {
Self {
name: "transfer_and_notify".to_string(),
gas,
}
}
}
pub trait SimpleTransfer {
/// Simple direct transfers between registered accounts.
///
/// Gas requirement: 5 TGas
/// Should be called by the balance owner.
/// Requires that the sender and the receiver accounts be registered.
///
/// Actions:
/// - Transfers `amount` of tokens from `predecessor_id` to `recipient`.
///
/// ## Panics
/// - if predecessor account is not registered - sender account
/// - if [recipient] account is not registered
/// - if sender account is same as receiver account
/// - if account balance has insufficient funds for transfer
fn transfer(
&mut self,
recipient: ValidAccountId,
amount: U128,
msg: Option<String>,
memo: Option<String>,
);
}
pub trait TransferCall {
/// Transfer `amount` of tokens from the predecessor account to a `recipient` contract.
/// The recipient contract MUST implement [TransferCallRecipient] interface. The tokens are
/// deposited but locked in the recipient account until the transfer has been confirmed by the
/// recipient contract and then finalized. The transfer workflow steps are:
/// 1. sender initiates the transfer via `transder_call`
/// 2. token transfers the funds from the sender's account to the recipient's account but locks
/// the transfer amount on the recipient account. The locked tokens cannot be used until
/// the recipient contract confirms the transfer.
/// 3. The recipient contract is then notified of the transfer via [`TransferCallRecipient::on_ft_receive`].
/// 4. Once the transfer notification call completes, then the [`TransferCallRecipient::on_ft_receive`]
/// callback on the token contract is invoked to finalize the transfer. If the recipient contract
/// successfully completed the transfer notification call, then the funds are unlocked
/// via the [`FinalizeTransferCallback::finalize_ft_transfer`] callback. If the [`TransferCallRecipient::on_ft_receive`]
/// call fails for any reason, then the fund transfer is rolled back in the finalize callback.
///
/// `msg`: is a message sent to the recipient. It might be used to send additional call
// instructions.
/// `memo`: arbitrary data with no specified format used to link the transaction with an
/// external event. If referencing a binary data, it should use base64 serialization.
///
/// ## Panics
/// - if accounts are not registered
/// - insufficient funds
fn transfer_call(
&mut self,
recipient: ValidAccountId,
amount: U128,
msg: Option<String>,
memo: Option<String>,
) -> Promise;
}
pub trait FinalizeTransferCallback {
/// Finalizes the token transfer
///
/// Actions:
/// - if the call `TransferCallRecipient::on_ft_receive` succeeds, then commit the transfer,
/// i.e., unlock the balance on the recipient account
/// - else rollback the transfer by returning the locked balance to the sender
///
/// #[private]
fn finalize_ft_transfer(&mut self, sender: AccountId, recipient: AccountId, amount: U128);
}
/// Interface for recipient call on fungible-token transfers.
/// `token` is an account address of the token - a smart-contract defining the token
/// being transferred.
/// `from` is an address of a previous holder of the tokens being sent
#[ext_contract(ext_transfer_call_recipient)]
pub trait TransferCallRecipient {
fn on_ft_receive(
&mut self,
from: ValidAccountId,
amount: U128,
msg: Option<String>,
memo: Option<String>,
);
}
#[ext_contract(ext_self_finalize_transfer_callback)]
pub trait ExtFinalizeTransferCallback {
/// Finalizes the token transfer
///
/// Actions:
/// - if the call `TransferCallRecipient::on_ft_receive` succeeds, then commit the transfer,
/// i.e., unlock the balance on the recipient account
/// - else rollback the transfer by returning the locked balance to the sender
///
/// #[private]
fn finalize_ft_transfer(&mut self, sender: AccountId, recipient: AccountId, amount: U128);
}
/// Implements [NEP-122 vault based fungible token standard](https://github.com/near/NEPs/issues/122)
/// with the following modifications:
/// - all token owners must be registered with the contract, which implies that token transfers can
/// only be between registered accounts
/// - this removes the need to require an attached deposit on each transfer because the accounts
/// are pre-registered
/// - eliminates transfers to non-existent accounts
/// - `transfer_raw` has been moved to [`SimpleTransfer::transfer`]
/// - `payload` has been replaced with `msg` and `memo` optional args
pub trait VaultBasedTransfer {
/// Transfer to a contract with payload
/// Gas requirement: 40+ TGas or 40000000000000 Gas.
/// Consumes: 30 TGas and the remaining gas is passed to the `recipient` (at least 10 TGas)
/// Should be called by the balance owner.
/// Returns a promise, that will result in the unspent balance from the transfer `amount`.
///
/// Actions:
/// - Withdraws `amount` from the `predecessor_id` account.
/// - Creates a new local safe with a new unique `safe_id` with the following content:
/// `{sender_id: predecessor_id, amount: amount, recipient: recipient}`
/// - Saves this safe to the storage.
/// - Calls on `recipient` method `on_token_receive(sender_id: predecessor_id, amount, safe_id, payload)`/
/// - Attaches a self callback to this promise `resolve_safe(safe_id, sender_id)`
///
/// ## Panics
/// - if predecessor account is not registered
/// - if [recipient] account is not registered
/// - if sender account is same as receiver account
/// - if account balance has insufficient funds for transfer
fn transfer_with_vault(
&mut self,
recipient: ValidAccountId,
amount: U128,
msg: Option<String>,
memo: Option<String>,
) -> Promise;
/// Withdraws from a given vault and transfers the funds to the specified receiver account ID.
///
/// Gas requirement: 5 TGas
/// Should be called by the contract that owns a given safe.
///
/// Actions:
/// - checks that the safe with `vault_id` exists and `predecessor_id == vault.recipient`
/// - withdraws `amount` from the vault or panics if `vault.amount < amount`
/// - deposits `amount` on the `recipient`
///
/// ## panics
/// - if predecessor account is not registered
/// - if predecessor account does not own the vault
/// - if [recipient] account is not registered
/// - if vault balance has insufficient funds for transfer
fn withdraw_from_vault(&mut self, vault_id: VaultId, recipient: ValidAccountId, amount: U128);
}
/// implements required callbacks defined in [ExtResolveVaultCallback]
pub trait ResolveVaultCallback {
/// Resolves a given vault, i.e., transfers any remaining vault balance to the sender account
/// and then deletes the vault. Returns the vault remaining balance.
///
/// Gas requirement: 5 TGas
///
/// Actions:
/// - Reads safe with `safe_id`
/// - Deposits remaining `safe.amount` to `sender_id`
/// - Deletes the safe
/// - Returns the total withdrawn amount from the safe `original_amount - safe.amount`.
/// #\[private\]
///
/// ## Panics
/// - if not called by self as callback
/// - following panics should never happen (if they do, then there is a bug in the code)
/// - if the sender account is not registered
/// - if the vault does not exist
fn resolve_vault(&mut self, vault_id: VaultId, sender_id: AccountId) -> U128;
}
/// Must be implemented by contracts that support [VaultBasedTransfer] token transfers
#[ext_contract(ext_token_receiver)]
pub trait ExtTokenVaultReceiver {
/// Called when a given amount of tokens is locked in a safe by a given sender with payload.
/// Gas requirements: 2+ BASE
/// Should be called by the fungible token contract
///
/// This methods should withdraw tokens from the safe and act on them. When this method returns a value, the
/// safe will be released and the unused tokens from the safe will be returned to the sender.
/// There are bunch of options what the contract can do. E.g.
/// - Option 1: withdraw and account internally
/// - Increase inner balance by `amount` for the `sender_id` of a token contract ID `predecessor_id`.
/// - Promise call `withdraw_from_safe(safe_id, recipient: env::current_account_id(), amount)` to withdraw the amount to this contract
/// - Return the promise
/// - Option 2: Simple redirect to another account
/// - Promise call `withdraw_from_safe(safe_id, recipient: ANOTHER_ACCOUNT_ID, amount)` to withdraw to `ANOTHER_ACCOUNT_ID`
/// - Return the promise
/// - Option 3: Partial redirect to another account (e.g. with commission)
/// - Promise call `withdraw_from_safe(safe_id, recipient: ANOTHER_ACCOUNT_ID, amount: ANOTHER_AMOUNT)` to withdraw to `ANOTHER_ACCOUNT_ID`
/// - Chain with (using .then) promise call `withdraw_from_safe(safe_id, recipient: env::current_account_id(), amount: amount - ANOTHER_AMOUNT)` to withdraw to self
/// - Return the 2nd promise
/// - Option 4: redirect some of the payments and call another contract `NEW_RECEIVER_ID`
/// - Promise call `withdraw_from_safe(safe_id, recipient: current_account_id, amount)` to withdraw the amount to this contract
/// - Chain with promise call `transfer_with_safe(recipient: recipient, amount: SOME_AMOUNT, payload: NEW_PAYLOAD)`
/// - Chain with the promise call to this contract to handle callback (in case we want to refund).
/// - Return the callback promise.
fn on_receive_with_vault(
&mut self,
sender_id: AccountId,
amount: U128,
vault_id: VaultId,
msg: Option<String>,
memo: Option<String>,
);
}
#[ext_contract(ext_self_resolve_vault_callback)]
pub trait ExtResolveVaultCallback {
/// Resolves a given vault - transfers vault remoining balance back to sender account and deletes
/// the vault.
///
/// Gas requirement: 5 TGas or 5000000000000 Gas
/// A callback. Should be called by this fungible token contract (`current_account_id`)
/// Returns the remaining balance.
///
/// Actions:
/// - Reads safe with `safe_id`
/// - Deposits remaining `safe.amount` to `sender_id`
/// - Deletes the safe
/// - Returns the total withdrawn amount from the safe `original_amount - safe.amount`.
/// #[private]
fn resolve_vault(&mut self, vault_id: VaultId, sender_id: AccountId) -> U128;
}
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, PartialEq)]
#[serde(crate = "near_sdk::serde")]
pub struct VaultId(pub U128);
impl From<u128> for VaultId {
fn from(value: u128) -> Self {
Self(value.into())
}
}
impl From<domain::VaultId> for VaultId {
fn from(id: domain::VaultId) -> Self {
Self(id.0.into())
}
} |
While vault-based token provides clean rollbacks, e.g. the receiver can't overspend the received amount, I'd propose to adapt a slightly modified NEP-136 like standard for Idea is to have the following interface: #[payable]
fn transfer_call(
&mut self,
receiver_id: AccountId,
amount: U128,
payload: String,
memo: Option<String>,
) -> Promise; It will do the following:
The receiver contract should return Now the It has the following pros and cons comparing to vault transfer:
CONS:
Token metadata, account registration, and balance view calls can be discussed separately. |
@evgenykuzyakov are you planning to add Re CONS:
|
I would propose consolidating all of the open FT related NEPs into a single NEP the FT discussion - and close out the rest. Let's create a new github repo that defines the contract interface. The goal of the new git repo would be that anybody that wants to implement the FT token standard interface can simply pull it into their project. Once we come to agreement on the contract interface then we can even officially publish the crate to https://crates.io |
No I don't think we should continue work on this NEP. At the same time #136 contains token metadata that I don't agree with. I would make sense to either remove some items from the metadata or remove metadata completely from the standard and move it to a different NEP. Also account registration should be a separate NEP. @robert-zaremba if you agree, I suggest split NEP#136 into 3 NEPs to separately discuss the following:
I'll start working on 3 new NEPs to cover each part. |
@evgenykuzyakov - yes, this makes sens. The final FT token will be a merge of the 3 other interfaces. |
Rational:
#[payable]
requirements, because it's not easily abusable. You'd have to transfer to non existing accounts. This can be addressed with minimum token balance.Background
There are a few reasons for allowance:
This can be solved by depositing tokens to the contract first and it can spend the tokens on your behalf.
withdraw from you when you initialized the transfer. This can be addressed through a callback.
Allowance is also often abused by dApps setting unlimited allowance all the time, so it defeats the purpose.
Safe-based transfers
Instead of having permanent allowance, we can introduce a one-time temporary allowance that only lives for the
duration of the transaction. We call this a safe. This idea is very similar to Auto-unlock with Safes idea, but it doesn't require protocol changes in the nearcore Runtime.
It works the following way:
transfer_with_safe
where the receiving side is a contract.amount
of tokens from the owner and temporary locks them.amount
of token from the temporary lock and use them.We call this temporary lock a
safe
.Implementation
Token interface
Receiving side interface
The text was updated successfully, but these errors were encountered: