Skip to content

Latest commit

 

History

History
337 lines (269 loc) · 17.2 KB

frc-0046.md

File metadata and controls

337 lines (269 loc) · 17.2 KB
fip title author discussions-to status type created
0046
Fungible token standard
Alex North (@anorth), Jeeva Suresh (@jsuresh), Alex Su (@alexytsu)
Draft
FRC
2022-08-17

FRC-0046: Fungible token standard

Simple Summary

A standard interface for native actor fungible tokens.

Abstract

This proposal provides a standard API for the implementation of fungible tokens as FVM native actors. Its design goal is a token API conceptually similar to prior art, including support for delegated control of tokens, but with superior safety and reduced cost and complexity.

The highlights are:

  • Standard token name/symbol/supply/balances, with a fixed decimal precision of 18.
  • Standard allowance protocol for delegated control, but with an API robust to front-running.
  • A universal receiver hook informing receiving actors of incoming tokens (mandatory).

The proposal learns from fungible token standards developed for other blockchain ecosystems. It is heavily inspired by ERC-777, but un-encumbered by backwards-compatibility concerns. The universal receiver hook is drawn from Lukso LSP-1.

Change 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.

This proposal utilises mandatory receiver hooks in order to:

  • prevent accidental transfers to actors unable to deal with the assets
  • reduce user interface complexity of a two-step allow/transfer-from interaction when transferring tokens to actors
  • avoid additional gas costs of the two-step transfer flow
  • reduce the need for users to provide "infinite" approvals to delegate actors as remedy for the above

Specification

Methods and types are described with a Rust-like pseudocode. All parameters and return types are IPLD types in a CBOR tuple encoding.

Methods are to be dispatched according to a calling convention such as that discussed in #382.

Interface FRC0046

An actor implementing an FRC-0046 token must provide the following methods.

// The type of a token amount is the same type used to represent the native Filecoin token.
// Token amounts serialize as a variable-length encoded integer, and have an implied 18 decimals precision.
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 smallest amount of tokens which is indivisible.
// All transfers, burns, and mints must be a whole multiple of the granularity.
// All balances must be a multiple of this granularity (but allowances need not be).
// Must be at least 1. Must never change.
// A granularity of 10^18 corresponds to whole units only, with no further decimal precision.
fn Granularity() -> uint64

// 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 should equal the sum of all minted tokens less the sum of all burnt tokens.
fn TotalSupply() -> TokenAmount

// Returns the balance of an address.
// Must be non-negative.
fn Balance(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.
// The operatorData is passed through to the receiver hook of the to address directly.
// Returns the resulting balances for the from and to addresses, and any data returned by the 
// receiver hook.
// Aborts if the receiver hook on the `to` address aborts.
fn Transfer({to: Address, amount: TokenAmount, operatorData: Bytes}) 
  -> {fromBalance: TokenAmount, toBalance: TokenAmount, recipientData: Bytes}

// Transfers tokens from one address to another.
// The caller must have previously been approved to control at least the sent amount.
// The caller's allowance is decreased by the transferred amount.
// The operatorData is passed through to the receiver hook of the to address directly.
// Returns the resulting balances for the from and to addresses, the operator's remaining allowance,
// and any data returned by the receiver hook.
fn TransferFrom({from: Address, to: Address, amount: TokenAmount, operatorData: Bytes}) 
  -> {fromBalance: TokenAmount, toBalance: TokenAmount, allowance: TokenAmount, recipientData: Bytes}

// Atomically increases the amount that an operator can transfer from the caller’s balance.
// The increase must be non-negative.
//
// Returns the new total allowance of the operator for the owner.
fn IncreaseAllowance({operator: Address, increase: TokenAmount}) -> TokenAmount

// Atomically decreases the amount that a operator 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 DecreaseAllowance({operator: Address, decrease: TokenAmount}) -> TokenAmount

// Sets the allowance a operator can transfer from the caller's balance to zero.
fn RevokeAllowance(operator: Address)

// Returns the allowance of an operator for an owner.
//
// The operator can burn or transfer the allowance amount from the owner's address.
fn Allowance({owner: Address, operator: Address}) -> TokenAmount

// Burns tokens from the caller’s balance, decreasing the total supply.
// Returns the caller's remaining balance.
fn Burn(amount: TokenAmount) -> {balance: TokenAmount}

// Burns tokens from an address’s balance.
// The caller must have previously been approved to control at least the burnt amount.
// Returns the remaining balance, and caller's remaing allowance.
fn BurnFrom({owner: Address, amount: TokenAmount}) -> {balance: TokenAmount, allowance: TokenAmount}

Interface FRC0046UniversalReceiver

An actor must implement the receiver hook in order to receive tokens.

This receiver hook is universal, in that it is extensible to future standards for transferable assets or data without necessarily requiring a code change in actors that implement it. An actor that wishes to accept all transfers may succeed without inspecting the payload.

/// Type of the payload accompanying the receiver hook for a FRC46 token.
struct FRC46TokenReceived {
  // The address from which tokens were debited
  from: Address,
  // The address to which tokens were credited (which will match the hook receiver)
  to: Address, 
  // The actor which initiated the mint or transfer
  operator: Address, 
  // The quantity of tokens received; non-negative
  amount: TokenAmount, 
  // Arbitrary data provided by the operator when initiating the transfer
  operatorData: Bytes, 
  // Arbitrary data provided by the token actor
  tokenData: Bytes,
}

/// Receiver hook type value for an FRC46 token.
const FRC46TokenType = frc42_hash("FRC46")

// Invoked by a token actor 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.
// Returns any data to be returned to the caller of the transfer method, 
// the schema of which may be specific to the token and receiver.
// Aborts if the receiver refuses the transfer.
fn Receive({type: uint32, payload: []byte}) -> Any

Behaviour

Universal receiver hook

A token must invoke the receiver hook method on the receiving address whenever it credits tokens. The type parameter must be FRC46TokenType and the payload must be the IPLD-CBOR serialized FRC46TokenReceived structure.

The credit may persist only if the receiver hook is implemented and does not abort. The mint or transfer operation should abort if the receiver hook does, or in any case must not credit tokens to that address.

Minting

API methods for minting are left unspecified. However, minting tokens to an address must invoke the receiver hook, and fail if it aborts.

Transfers

Transfers of zero amounts are allowed, including when the from address has zero balance. A transfer of zero must invoke the receiver hook of the to address, and abort if the hook aborts. A transfer of zero can thus be used to send a message to an actor in the context of a specific token.

Operators

An operator must fail to act on behalf of an owner if its allowance is less than the requested amount. The method must abort with exit code USR_FORBIDDEN (18). An operator must always fail to act on behalf of an owner if it has zero allowance for that owner, even if the amount to transfer is zero. The allowance check takes precedence over other state preconditions. A token must not invoke a receiver hook specifying an operator that has zero allowance prior to the attempted transfer.

Addresses

Addresses for receivers and operators must be resolvable to an actor ID. Balances must only be credited to an actor ID. All token methods must attempt to resolve addresses provided in parameters to actor IDs. A token should attempt to initialise an account for any address which cannot be resolved by sending a zero-value transfer of the native token to the address.

Note that this means that an uninitialized actor-type (f2) address cannot receive tokens or be authorized as an operator. Future changes to the FVM may permit initialization of such addresses by sending a message to them, in which case they should automatically become functional for this standard.

Extensions

A token may implement other methods for transferring tokens and managing allowances. These must maintain the invariants about supply and balances, and invoke the receiver hook when crediting tokens.

A token may implement restrictions on allowances and transfer of tokens.

A token may implement changes in balances other than those strictly implied by method parameters, such as a rebasing token that burns some units on every transfer. Such a token must maintain the invariant that total supply is the sum of balances.

Design Rationale

Decimals

There is no decimals() function. All token amounts are assumed to have a precision of 18 decimal places, matching that of the native Filecoin token.

Tokens that desire a more course granularity (e.g. in-game asset units) should return a Granularity value greater than 1 and enforce balances conform to that granularity internally (e.g. rejecting attempts to transfer or mint fractional tokens).

Receiver hook

This standard requires the actor receiving tokens to implement a receiver hook to accept the transfer.

This mechanism achieves two significant outcomes:

  • prevents loss-of-value from transferring tokens to actors that cannot use them; and
  • notifies receiving actors so they can perform internal logic in response

Receiving tokens becomes an opt-in capability, and most actors not designed to receive tokens will not implement a receiver. Invoking the receiver hook method will thus abort (e.g. with a USR_UNHANDLED_MESSAGE code) and cancel the transfer. This will prevent classes 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.

  • Without hooks, the typical flow is for the depositor to first approve the receiving contract (often for an infinite allowance), wait for that approval, then call a method on the contract which internally invokes TransferFrom() to "pull" the tokens, and then update internal records.
  • With hooks, the depositor simply calls Transfer(), perhaps with some attached metadata. If the receiving contract accepts the deposit, it can update internal state; or it can reject the deposit by aborting.

Receiver hooks thus use less gas, provide a simpler UX, and prevent a large class of user errors.

Universality

The single receiver hook that accepts a “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! Each should specify a distinct asset type code via an FRC-42 hash.

Implementing the universal receiver hook in the built-in account and multisig actors (to always succeed) will permit the development of new standards without the need for complex community coordination.

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.

Built-in actor support

For this proposal to be adopted as standard, the built-in account and multisig actors must implement the receiver hook (as a no-op). This implies adoption of the associated calling convention, too (more discussion in #401).

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:

  • Token contracts are free to implement additional methods, including the ERC-20 standard for direct calls from existing EVM contracts. It’s likely feasible to decode EVM ABI messages in native actors to support direct calls.
  • A bridge actor could mediate token balances between two environments. This would function much like existing cross-chain or layer-two bridges in other blockchains.

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.

Backwards Compatibility

There are no implementations of tokens yet on Filecoin.

This proposal shares semantics with ERC-777 and thus ERC-20, but is not binary-compatible. The primary reason for this incompatibility is the different primitive types supported by the underlying VM. The EVM has a native word size of 256 bits, but the FVM uses 64 bit words. A second similarly-important incompatibility is use of the conventional IPLD-CBOR encoding on Filecoin, vs the EVM's Solidity ABI standard. The simple, efficient, and canonical operation of FVM-native contracts is taken as more important than binary EVM compatibility.

Test Cases

Extensive test cases are present in the implementation of this proposal at https://github.com/helix-onchain/filecoin/tree/main/fil_fungible_token.

These can be duplicated into this proposal if warranted.

Security Considerations

This proposal explicitly addresses some security weaknesses of prior art.

  • Race conditions in ERC-20's Approve method are remedied by specifying an allowance delta.
  • Losing tokens by transferring them to an incapable address is prevented by receiver hooks.
  • Infinite allowances are unnecessary if contracts implement rich receiver hooks rather than the two-step approve/transfer-from transfer model.

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.

Incentive Considerations

N/A.

Product Considerations

This fungible token standard could be adopted by the built-in actors to represent data cap (FIP-0045). This would enshrine the standard, but permit data cap to be recognized by wallets and other on-chain and external software.

Implementation

An implementation of this standard in development at https://github.com/helix-onchain/filecoin/tree/main/fil_fungible_token.

Copyright

Copyright and related rights waived via CC0.