Native fungible token standard (FRC-0046) #407
Replies: 8 comments 27 replies
-
Not much comments on Fungible Token Standards, do have a lot to say on Non Fungible Token Standards. But I do notice many new chains are using u128/u64 over BigInt, simply because they are primitives and more than sufficient to cover all use cases. |
Beta Was this translation helpful? Give feedback.
-
One design question is whether the token receiver hook should receive addresses as Address type or ActorID type.
Not sure how to resolve this tension between convention and a probably-superior approach. |
Beta Was this translation helpful? Give feedback.
-
Moving discussion about granularity out from PR
|
Beta Was this translation helpful? Give feedback.
-
I think TokenAmount needs to be strictly defined.
In the draft, mentioned that tokenAmount uses var-int serialization, does this mean that can't use big.Int directly in golang. If not, can use the (uint64, uint64) scheme? var-int does not seem to work in the case of large numbers. |
Beta Was this translation helpful? Give feedback.
-
A design question to consider is whether the ERC-777 specifies separate fields for |
Beta Was this translation helpful? Give feedback.
-
A thread about universal receiver hook. Quoting FRC: This is a single receiver hook that accepts an additional “type” parameter identifying the type of asset being received.
We already intend to specify a non-fungible token API as companion to this one, with its own receiver hook if necessary. |
Beta Was this translation helpful? Give feedback.
-
I think we should consider naming all the methods with an |
Beta Was this translation helpful? Give feedback.
-
One new thought prompted by exploration of NFT APIs. There is currently no standard method to enumerate the balances (or allowances) of a token. Contrast with something like ERC-721 that at least specifies an optional method for enumeration. I think we should add enumeration to FRC-0046, at least as optional but preferably as a required part of the standard. One might claim that the kind of users that need to enumerate are off-chain and can use direct state inspection, but (a) the kind of users that we know about are off-chain today because ERC-20 doesn't support enumeration, and (b) that's a very rough solution anyway. The easiest way for actor introspection off-chain is to invoke read-only methods on the actor/contract anyway (e.g. over RPC to a node). Direct state inspection more-or-less requires the node to implement an understanding of the state of individual actors, which is not reasonable for most user-programmed actors. Even though we have a reference implementation, tokens might embed that state in other actor state (as the Datacap actor does), or are free to use a different representation too. Actor authors should be able to fully abstract their state representation without suffering reduced amenity to tooling, explorers etc, and do so via an API they define. See also #383 So
I propose we add methods to enumerate the balance and allowance tables. Note: The built-in Datacap actor (FIP-0045 in nv17) doesn't yet implement such methods, but it'll be easy to add them so it conforms to an updated standard. |
Beta Was this translation helpful? Give feedback.
-
See https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md
[Edit 2022-08-02: built-in multisig should also implement receiver hook]
This is a proposal for a fungible token standard for native actors. It’s a collaborative effort with @jsuresh and @alexytsu.
Motivation
The concept of a fungible token is widely established in other blockchains. As in those ecosystems, the Filecoin actor ecosystem will benefit from a standard API implemented by most tokens. A standard permits easy building of UIs, wallets, tools, and higher level applications on top of a variety of tokens representing different quantities.
Early token APIs like ERC-20 were developed before many applications had been built, and so fall short in a number of ways. Network effects have led to significant adoption and lock-in, but even the authors acknowledge that they’d do it differently if starting over. Filecoin has a unique opportunity to benefit from the learnings of other ecosystems and, at least for native actors, establish more powerful and flexible fundamental building blocks.
Goals
This proposal aims to:
Proposal
This proposal is inspired significantly by ERC-777, an improved token standard for Ethereum. The Filecoin proposal is unencumbered by backward compatibility concerns.
The highlights are:
API specification
Methods and types are described with a Rust-like pseudocode. All parameters and return types are IPLD types in a CBOR tuple encoding. The spec below is not exhaustive with respect to all possible inputs and states; a FIP to be drafted after discussion of these ideas should fill all the gaps.
Methods are to be presented according to the calling convention such as that discussed in #382, so this recommendation depends on that.
Interface FRC-XXX
type TokenAmount = BigInt
/// Returns the name of the token.
/// Must not be empty.
fn name() -> String
/// Returns the ticker symbol of the token.
/// Must not be empty. Should be a short string of uppercase.
fn symbol() -> String
/// Returns the total amount of the token in existence.
/// Must be non-negative.
/// The total supply must equal the balances of all addresses.
/// The total supply must equal the sum of all minted tokens
/// less the sum of all burnt tokens.
fn total_supply() -> TokenAmount
/// Returns the balance of an address.
/// Must be non-negative.
fn balance_of(owner: Address) -> TokenAmount
/// Transfers tokens from caller to another address.
/// Amount must be non-negative (but can be zero).
/// Transferring to the caller must be treated as a normal transfer.
/// Fails (aborts) if invoking the receiver hook on the
/// to address aborts.
fn transfer({to: Address, amount: TokenAmount, data: Bytes})
/// Transfers tokens from one address to another.
/// The caller must have previously been approved to control
/// at least the sent amount.
fn transfer_from({from: Address, to: Address, amount: TokenAmount, data: Bytes})
/// Atomically increases the amount that a spender can transfer
/// from the caller’s balance.
/// The increase must be non-negative.
///
/// Returns the new total allowance of the spender for the owner.
fn increase_allowance({spender: Address, increase: TokenAmount}) -> TokenAmount
/// Atomically decreases the amount that a spender can transfer
/// from the caller’s balance.
/// The decrease must be non-negative.
///
/// Sets the remaining allowance to zero if the decrease is
/// more than the current allowance.
fn decrease_allowance({spender: Address, decrease: TokenAmount}) -> TokenAmount
/// Sets the allowance a spender can transfer from the caller’s
/// balance to zero.
fn revoke_allowance(spender: Address)
/// Returns the allowance of a spender for an owner.
///
/// The spender can burn or transfer the allowance amount
/// out of the owner's address.
fn allowance({owner: Address, spender: Address}) -> TokenAmount
/// Burns tokens from the caller’s balance, decreasing the
/// total supply.
fn burn(amount: TokenAmount)
/// Burns tokens from an address’s balance.
/// The caller must have previously been approved to control
/// at least the burnt amount.
fn burnFrom({owner: Address, amount: TokenAmount})
Interface FRCXXXTokenReceiver
See also the universal receiver hook idea, below.
/// Invoked by a token contract after transfer of tokens to the
/// receiver’s address.
/// The token state must be persisted such that the hook receiver
/// will observe the new balances.
/// Amount must be non-negative (but may be zero).
/// Aborts if the receiver refuses the transfer.
fn token_received({from: Address, amount: TokenAmount, data: Bytes})
Behaviour
A token may implement other methods for transferring tokens and managing allowances. These must maintain the invariants about supply and balances.
Transferring tokens
This standard requires the actor receiving tokens to implement a receiver hook to accept the transfer. This mechanism achieves two significant outcomes:
Because receiving tokens becomes an opt-in capability, it’s expected that most actors not designed to receive tokens will not implement a receiver. Invoking that method will thus abort (e.g. with a USR_UNHANDLED_MESSAGE code) and cancel the transfer. This will prevent common cases of error such as transferring to the token actor itself, an exchange router, etc.
A receiver hook significantly simplifies the common flow of depositing tokens into some actor (e.g. an investment vault) that needs to perform some internal accounting in response.
Receiver hooks thus use less gas, provide a simpler UX, and prevent a large class of user errors.
Minting
API methods for minting are left unspecified. However, minting tokens to an address must invoke the receiver hook, and fail if it aborts.
Addresses
Addresses for receivers and spenders of tokens must be resolvable to an actor ID. See discussion about built-in account actor support, but some implications:
We might wish to re-evaluate some of these implications, but any alternative policy will involve more complexity in token implementations.
Discussion
Decimals
There is no decimals() function. All tokens are assumed to have a precision of 18 decimal places, matching that of the native Filecoin token.
Tokens that should logically have a lesser precision (e.g. in-game asset units) should enforce balances conform to their desired precision internally (e.g. rejecting attempts to transfer or mint fractional tokens).
Approvals and allowances
The approval mechanism is retained in order to handle asynchronous workflows, where tokens are pulled some time after the approval, or as part of more complicated transactions.
Reentrancy
Receiver hooks introduce the possibility of complex call flows, the most concerning of which might be a malicious receiver that calls back to a token actor in an attempt to exploit a re-entrancy bug. We expect that one or more high quality reference implementations of the token state and logic will keep the vast majority of token actors safe. We judge the risk to more complex actors as lesser than the aggregate risk of losses due to misdirected transfers and infinite approvals.
Built-in actor support
For this proposal to be adopted as standard, we would need the built-in account and multisig actors to implement the receiver hook (as a no-op). This implies adoption of the associated calling convention, too (more discussion in #401). This should also support transfer of tokens to uninitialised public-key-addressed actors (that have never sent a message), as the VM will install a built-in account actor at the address during the process of invoking the hook.
This would not automatically support transfer to uninitialised non-public-key addresses, such as those deterministically generated for future actor installation (#379). Support could be added by treating the NotFound syscall error as success. This would however create potential for lost tokens transferred to such addresses. We should decide and specify this behaviour.
EVM support
We need to see more detail of proposals for FVM/EVM (and other hosted VM) integration in order to design smooth compatibility. Pending such details, a few paths for token integration present themselves:
Depending again on details about EVM account actors, it may be possible to transfer native tokens to EVM addresses directly if they implement the receiver hook. If not, the fact that EVM account actors would not implement the token receiver hook would likely save many people from an otherwise easy error of confusing the address types or considering such transfer valid.
If bridging, the bridge actor would implement the hook but may not be able to confirm the receiving EVM contract can in fact handle the tokens. This is a necessary loss of functionality if “downgrading” to ERC-20 semantics.
Extension - universal receiver
The Lukso blockchain (from ERC-20 author @frozeman) generalises the token receiver hook to a universal receiver. This is a single receiver hook that accepts an additional “type” parameter identifying the type of asset being received. This permits a single hook to respond to all kinds of asset transfers (e.g. non-fungible tokens), including those which have not yet been specified!
/// Universal receiver hook.
/// The data bytes should be decoded according to the asset type.
fn asset_received({type: String, data: Bytes})
We already intend to specify a non-fungible token API as companion to this one, with its own receiver hook if necessary. But the flexibility to support future standards is very attractive. For example, if we implement a universal receiver in the built-in account actor, future standards can emerge easily. Without that, developing a new standard would require large-scale community buy-in before it could be realistically tried out.
Extension - sender hooks
ERC-777 also specifies a token sender hook. This calls a hook in the owner account whenever tokens are transferred, and similarly permits cancelling the transfer. We might consider whether such mechanism is worthwhile to establish from the beginning for Filecoin.
Beta Was this translation helpful? Give feedback.
All reactions