From 777f2f1acc88dd015e42066bf29b3d40026bc1d7 Mon Sep 17 00:00:00 2001 From: anorth <445306+anorth@users.noreply.github.com> Date: Mon, 31 Jan 2022 13:10:10 +1100 Subject: [PATCH 1/3] First draft of fungible token standard. --- FRCs/frc-tokenstandard.md | 290 ++++++++++++++++++++++++++++++++++++++ templates/template_FTP.md | 2 +- 2 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 FRCs/frc-tokenstandard.md diff --git a/FRCs/frc-tokenstandard.md b/FRCs/frc-tokenstandard.md new file mode 100644 index 000000000..bdf56323b --- /dev/null +++ b/FRCs/frc-tokenstandard.md @@ -0,0 +1,290 @@ +--- +fip: +title: Fungible token standard +author: Alex North (@anorth), Jeeva Suresh (@jsuresh), Alex Su (@alexytsu) +discussions-to: https://github.com/filecoin-project/FIPs/discussions/277 +status: Draft +type: FRC +created: 2022-08-17 +--- + +## 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](https://ieeexplore.ieee.org/document/8802438). +- Receiver hooks 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](https://eips.ethereum.org/EIPS/eip-777), +but un-encumbered by backwards-compatibility concerns. + +## 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](https://eips.ethereum.org/EIPS/eip-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](https://thedefiant.io/fabian-vogelsteller-father-of-the-erc20-standard/) 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 presented according to a calling convention such as that discussed in [#382](https://github.com/filecoin-project/FIPs/discussions/382), +so this recommendation depends on that. + +### Interface FRC-XXX + +``` +// Token amounts serialize as var-int, and have an implied 18 decimals precision, +// matching the native token. +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 should 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. +// Returns the sender's remaining balance. +// Aborts if the receiver hook on the `to` address aborts. +fn transfer({to: Address, amount: TokenAmount, data: Bytes}) -> TokenAmount + +// 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. +fn transfer_from({from: Address, to: Address, amount: TokenAmount, data: Bytes}) -> TokenAmount + +// 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 increase_allowance({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 decrease_allowance({operator: Address, decrease: TokenAmount}) -> TokenAmount + +// Sets the allowance a operator can transfer from the caller's balance to zero. +fn revoke_allowance(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) -> 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}) -> TokenAmount +``` + +### Interface FRCXXXTokenReceiver +See also the universal receiver hook idea, below. + +``` +// 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. +// Amount must be non-negative (but may be zero). +// Aborts if the receiver refuses the transfer. +fn token_received({from: Address, to: Address, operator: 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, +and should 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. + +#### 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. + +## Design Rationale + +### 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). + +### 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. + +### 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](https://github.com/filecoin-project/FIPs/discussions/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. + +### 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. + +## 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 datacap (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](https://creativecommons.org/publicdomain/zero/1.0/). \ No newline at end of file diff --git a/templates/template_FTP.md b/templates/template_FTP.md index 0ac6038ae..87faf906d 100644 --- a/templates/template_FTP.md +++ b/templates/template_FTP.md @@ -4,7 +4,7 @@ title: author: , FirstName (@GitHubUsername) and GitHubUsername (@GitHubUsername)> discussions-to: status: Draft -type: +type: category (*only required for Standard Track): created: spec-sections: From a08ef58009e6b63b1da2cc932cef9ec8ea5ac17f Mon Sep 17 00:00:00 2001 From: anorth <445306+anorth@users.noreply.github.com> Date: Mon, 22 Aug 2022 20:10:17 +1200 Subject: [PATCH 2/3] Add granularity method. Expand return values --- FRCs/{frc-tokenstandard.md => frc-0046.md} | 44 ++++++++++++++-------- README.md | 1 + 2 files changed, 30 insertions(+), 15 deletions(-) rename FRCs/{frc-tokenstandard.md => frc-0046.md} (88%) diff --git a/FRCs/frc-tokenstandard.md b/FRCs/frc-0046.md similarity index 88% rename from FRCs/frc-tokenstandard.md rename to FRCs/frc-0046.md index bdf56323b..e0dc9fa71 100644 --- a/FRCs/frc-tokenstandard.md +++ b/FRCs/frc-0046.md @@ -1,5 +1,5 @@ --- -fip: +fip: 0046 title: Fungible token standard author: Alex North (@anorth), Jeeva Suresh (@jsuresh), Alex Su (@alexytsu) discussions-to: https://github.com/filecoin-project/FIPs/discussions/277 @@ -50,14 +50,14 @@ This proposal utilises mandatory receiver hooks in order to: 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 presented according to a calling convention such as that discussed in [#382](https://github.com/filecoin-project/FIPs/discussions/382), +Methods are to be dispatched according to a calling convention such as that discussed in [#382](https://github.com/filecoin-project/FIPs/discussions/382), so this recommendation depends on that. -### Interface FRC-XXX +### Interface FRC-0046 ``` -// Token amounts serialize as var-int, and have an implied 18 decimals precision, -// matching the native token. +// 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. @@ -68,6 +68,13 @@ fn name() -> String // 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. @@ -81,14 +88,18 @@ 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. -// Returns the sender's remaining balance. +// Returns the resulting balances for the from and to addresses. +// The operatorData is passed through to the receiver hook directly. // Aborts if the receiver hook on the `to` address aborts. -fn transfer({to: Address, amount: TokenAmount, data: Bytes}) -> TokenAmount +fn transfer({to: Address, amount: TokenAmount, operatorData: Bytes}) + -> {fromBalance: TokenAmount, toBalance: TokenAmount} // 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. -fn transfer_from({from: Address, to: Address, amount: TokenAmount, data: Bytes}) -> TokenAmount +// Returns the resulting balances for the from and to addresses, and operator's remaining allowance. +fn transfer_from({from: Address, to: Address, amount: TokenAmount, operatorData: Bytes}) + -> {fromBalance: TokenAmount, toBalance: TokenAmount, allowance: TokenAmount} // Atomically increases the amount that an operator can transfer from the caller’s balance. // The increase must be non-negative. @@ -112,22 +123,25 @@ 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) -> TokenAmount +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. -fn burnFrom({owner: Address, amount: TokenAmount}) -> TokenAmount +// Returns the remaining balance, and caller's remaing allowance. +fn burnFrom({owner: Address, amount: TokenAmount}) -> {balance: TokenAmount, allowance: TokenAmount} ``` -### Interface FRCXXXTokenReceiver +### Interface FRC0046TokenReceiver See also the universal receiver hook idea, below. ``` // 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. +// The tokenData is arbitrary data originating from the token actor. +// The operatorData is the data value provided by the actor initiating the transfer. // Amount must be non-negative (but may be zero). // Aborts if the receiver refuses the transfer. -fn token_received({from: Address, to: Address, operator: Address, amount: TokenAmount, data: Bytes}) +fn token_received({from: Address, to: Address, operator: Address, amount: TokenAmount, operatorData: Bytes, tokenData: Bytes}) ``` ### Behaviour @@ -237,7 +251,7 @@ 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}) +fn asset_received({type: String, data: Bytes, operatorData: Bytes}) ``` We already intend to specify a non-fungible token API as companion to this one, with its own receiver hook if necessary. @@ -280,8 +294,8 @@ We judge the risk to more complex actors as lesser than the aggregate risk of lo N/A. ## Product Considerations -This fungible token standard could be adopted by the built-in actors to represent datacap (FIP-0045). -This would enshrine the standard, but permit data-cap to be recognized by wallets and other on-chain and external software. +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. diff --git a/README.md b/README.md index 52ed13676..a77f9162c 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,4 @@ This improvement protocol helps achieve that objective for all members of the Fi |[0041](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0041.md) | Forward Compatibility for PreCommit and ReplicaUpdate | FIP |@Kubuxu |Draft | |[0042](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md) | Calling Convention for Hashed Method Name | FRC |@Kubuxu, @anorth |Draft | |[0045](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0045.md) | De-couple verified registry from markets | FIP |@anorth |Draft | +|[0046](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0046.md) | Fungible token standard | FRC |@anorth, @jsuresh, @alexytsu |Draft | From ed968e422f160af23c0534c6c7c9488a15380638 Mon Sep 17 00:00:00 2001 From: anorth <445306+anorth@users.noreply.github.com> Date: Tue, 30 Aug 2022 13:32:00 +1200 Subject: [PATCH 3/3] Universal receiver hook. FRC42-compatible method names. --- FRCs/frc-0046.md | 126 ++++++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/FRCs/frc-0046.md b/FRCs/frc-0046.md index e0dc9fa71..18ceca791 100644 --- a/FRCs/frc-0046.md +++ b/FRCs/frc-0046.md @@ -20,11 +20,12 @@ 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](https://ieeexplore.ieee.org/document/8802438). -- Receiver hooks informing receiving actors of incoming tokens (mandatory). +- 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](https://eips.ethereum.org/EIPS/eip-777), but un-encumbered by backwards-compatibility concerns. +The universal receiver hook is drawn from Lukso [LSP-1](https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-1-UniversalReceiver.md). ## Change Motivation The concept of a fungible token is widely established in other blockchains. @@ -50,10 +51,10 @@ This proposal utilises mandatory receiver hooks in order to: 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](https://github.com/filecoin-project/FIPs/discussions/382), -so this recommendation depends on that. +Methods are to be dispatched according to a calling convention such as that discussed in [#382](https://github.com/filecoin-project/FIPs/discussions/382). -### Interface FRC-0046 +### 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. @@ -62,28 +63,28 @@ type TokenAmount = BigInt // Returns the name of the token. // Must not be empty. -fn name() -> String +fn Name() -> String // Returns the ticker symbol of the token. // Must not be empty. Should be a short string of uppercase. -fn symbol() -> String +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 +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 total_supply() -> TokenAmount +fn TotalSupply() -> TokenAmount // Returns the balance of an address. // Must be non-negative. -fn balance_of(owner: Address) -> TokenAmount +fn Balance(owner: Address) -> TokenAmount // Transfers tokens from caller to another address. // Amount must be non-negative (but can be zero). @@ -91,69 +92,89 @@ fn balance_of(owner: Address) -> TokenAmount // Returns the resulting balances for the from and to addresses. // The operatorData is passed through to the receiver hook directly. // Aborts if the receiver hook on the `to` address aborts. -fn transfer({to: Address, amount: TokenAmount, operatorData: Bytes}) +fn Transfer({to: Address, amount: TokenAmount, operatorData: Bytes}) -> {fromBalance: TokenAmount, toBalance: TokenAmount} // 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. // Returns the resulting balances for the from and to addresses, and operator's remaining allowance. -fn transfer_from({from: Address, to: Address, amount: TokenAmount, operatorData: Bytes}) +fn TransferFrom({from: Address, to: Address, amount: TokenAmount, operatorData: Bytes}) -> {fromBalance: TokenAmount, toBalance: TokenAmount, allowance: TokenAmount} // 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 increase_allowance({operator: Address, increase: TokenAmount}) -> TokenAmount +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 decrease_allowance({operator: Address, decrease: TokenAmount}) -> TokenAmount +fn DecreaseAllowance({operator: Address, decrease: TokenAmount}) -> TokenAmount // Sets the allowance a operator can transfer from the caller's balance to zero. -fn revoke_allowance(operator: Address) +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 +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} +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} +fn BurnFrom({owner: Address, amount: TokenAmount}) -> {balance: TokenAmount, allowance: TokenAmount} ``` -### Interface FRC0046TokenReceiver -See also the universal receiver hook idea, below. +### 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. -// The tokenData is arbitrary data originating from the token actor. -// The operatorData is the data value provided by the actor initiating the transfer. -// Amount must be non-negative (but may be zero). // Aborts if the receiver refuses the transfer. -fn token_received({from: Address, to: Address, operator: Address, amount: TokenAmount, operatorData: Bytes, tokenData: Bytes}) +fn Receive({type: uint32, payload: []byte}) ``` ### Behaviour -A token may implement other methods for transferring tokens and managing allowances. -These must maintain the invariants about supply and balances, -and should invoke the receiver hook when crediting tokens. -A token may implement restrictions on allowances and transfer of tokens. +#### 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. -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. +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. @@ -184,14 +205,27 @@ 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 tokens are assumed to have a precision of 18 decimal places, matching that of the native Filecoin token. +All token amounts 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). +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. @@ -216,6 +250,15 @@ A receiver hook significantly simplifies the common flow of depositing tokens in 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. @@ -242,23 +285,6 @@ If bridging, the bridge actor would implement the hook but may not be able to co 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, operatorData: 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. - ## Backwards Compatibility There are no implementations of tokens yet on Filecoin.