AEX: 9
Title: Fungible Token Standard
Author: @mradkov, @thepiwo
License: ISC
Discussions-To: https://forum.aeternity.com/t/aex-9-fungible-token/3565
Status: Review
Type: Standards Track
Created: 2019-05-11
This document aims to outline a standard and define how fungible tokens should be created and used on aeternity blockchain.
The following standard allows for the implementation of a standard API for tokens within smart contracts. This standard provides basic functionality to transfer tokens, as well as allow tokens to be approved so they can be spent by another on-chain third party.
This standard will allow decentralized applications and wallets to handle fungible tokens in a standardized way. A standard interface allows any tokens to be re-used by other applications, e.g. from wallets to decentralized exchanges.
The provided specification is following the ERC20 standard introduced in Ethereum for fungible tokens. This standard is proven to be working, it will help with interoperability and easier developer onboarding as the main differences will be Sophia syntax related. Another goal with following the ERC20 standard, is to learn from its mistakes and not repeat them.
The newly proposed standard should be easy to use but extendable with more functionality, both first and third party as shown by splitting allowance, minting, burning and swapping into optional extensions.
contract interface FungibleTokenInterface =
record meta_info =
{ name : string
, symbol : string
, decimals : int }
datatype event =
Transfer(address, address, int)
entrypoint aex9_extensions : () => list(string)
entrypoint meta_info : () => meta_info
entrypoint total_supply : () => int
entrypoint owner : () => address
entrypoint balances : () => map(address, int)
entrypoint balance : (address) => option(int)
entrypoint transfer : (address, int) => unit
This function returns a hardcoded list of all implemented extensions on the deployed contract.
entrypoint aex9_extensions() : list(string)
This function returns meta information associated with the token contract.
entrypoint meta_info() : meta_info
return | type |
---|---|
meta_info | meta_info |
record meta_info =
{ name : string
, symbol : string
, decimals : int }
This function returns the total token supply.
entrypoint total_supply() : int
return | type |
---|---|
total_supply | int |
This function returns the full balance state for static calls, e.g. by a blockchain explorer.
entrypoint balances() : map(address, int)
return | type |
---|---|
balances | map(address, int) |
This function returns the account balance of another account with address owner
, if the account exists. If the owner address is unknown to the contract None
will be returned. Using option
type as a return value allows us to determine if the account has balance of 0, more than 0, or the account has never had balance and is still unknown to the contract.
entrypoint balance(owner: address) : option(int)
parameter | type |
---|---|
owner | address |
return | type |
---|---|
balance | option(int) |
This function allows transfer of value
amount of tokens to to_account
address and MUST fire the Transfer
event. The function SHOULD abort if the Call.caller's account balance does not have enough tokens to spend.
Note: Transfers of 0 values MUST be treated as normal transfers and fire the Transfer
event.
stateful entrypoint transfer(to_account: address, value: int) : unit
parameter | type |
---|---|
to_account | address |
value | int |
This event MUST be triggered and emitted when tokens are transferred, including zero value transfers.
The transfer event arguments should be as follows: (from_account, to_account, value)
Transfer(address, address, int)
parameter | type |
---|---|
from_account | address |
to_account | address |
value | int |
Mint (optional if token creation happens) - MUST trigger when tokens are minted and thus are newly available in the given token contract, this also applies to tokens created using the init
method on contract creation.
The mint event arguments should be as follows: (account, value)
Mint(address, int)
parameter | type |
---|---|
account | address |
value | int |
This section covers the extendability of the basic token - e.g. mintable, burnable and allowances.
When a token contract implements an extension its name should be included in the aex9_extensions
array, in order for third party software or contracts to know the interface.
Any extensions should be implementable without permission. Developers of extensions MUST choose a name for aex9_extensions
that is not yet used. Developers CAN make a pull request to the reference implementation for general purpose extensions and maintainers choose to eventually include them.
This function mints value
new tokens to account
. The function SHOULD abort if Call.caller
is not the owner of the contract state.owner
.
stateful entrypoint mint(account: address, value: int) : unit
parameter | type |
---|---|
account | address |
value | int |
Mint - MUST trigger when tokens are minted and thus are newly available in the given token contract, this also applies to tokens created using the init
method on contract creation.
The mint event arguments should be as follows: (account, value)
Mint(address, int)
parameter | type |
---|---|
account | address |
value | int |
This function burns value
of tokens from Call.caller
.
stateful entrypoint burn(value: int) : unit
parameter | type |
---|---|
value | int |
Burn - MUST trigger when tokens are burned and thus are no longer available in the given token contact.
The burn event arguments should be as follows: (account, value)
Burn(address, int)
parameter | type |
---|---|
account | address |
value | int |
Allows for_account
to withdraw from your account multiple times, up to the value
amount. If this function is called again it overwrites the current allowance with value
.
Note: To prevent attack vectors (like the ones possible in ERC20) clients SHOULD make sure to create user interfaces in such a way that they set the allowance first to 0 before setting it to another value for the same spender. THOUGH the contract itself shouldn't enforce it, to allow backwards compatibility with contracts deployed before.
stateful entrypoint create_allowance(for_account: address, value: int) : unit
parameter | type |
---|---|
for_account | address |
value | int |
Transfers value
amount of tokens from address from_account
to address to_account
, and MUST fire the Transfer
event.
The transfer_allowance
method is used for a withdraw workflow, allowing contracts to transfer tokens on your behalf. This can be used for example to allow a contract to transfer tokens on your behalf and/or to charge fees in sub-currencies. The function SHOULD abort unless the from_account
account has deliberately authorized the sender of the message via some mechanism.
Note: Transfers of 0 values MUST be treated as normal transfers and fire the Transfer
event.
stateful entrypoint transfer_allowance(from_account: address, to_account: address, value: int)
parameter | type |
---|---|
from_account | address |
to_account | address |
value | int |
This function returns the amount which for_account
is still allowed to withdraw from from_account
, where record allowance_accounts = { from_account: address, for_account: address }
. If no allowance for this combination of accounts exists, None
is returned.
entrypoint allowance(allowance_accounts : allowance_accounts) : option(int)
parameter | type |
---|---|
allowance_accounts | allowance_accounts |
record allowance_accounts =
{ from_account: address
, for_account: address }
This function returns all allowances stored in state.allowances
record.
entrypoint allowances() : allowances
return | type |
---|---|
allowances | map(allowance_accounts, int) |
This function will look up the allowances and return the allowed spendable amount from from_account
for the transaction sender Call.caller
. If there is no such allowance present result is None
, otherwise Some(int)
is returned with the allowance amount.
entrypoint allowance_for_caller(from_account: address) : option(int)
parameter | type |
---|---|
from_account | address |
return | type |
---|---|
option(int) |
This function allows the Call.caller
to change the allowed spendable value for for_account
with value_change
. This adds the value_change
to the current allowance value. If used for increasing allowance amount a positive value should be passed, if the desired outcome is to lower the value of the allowed spendable value a negative value_change
should be passed.
stateful entrypoint change_allowance(for_account: address, value_change: int)
parameter | type |
---|---|
for_account | address |
value_change | int |
return | type |
---|---|
unit |
Resets the allowance given for_account
to zero.
stateful entrypoint reset_allowance(for_account: address)
parameter | type |
---|---|
for_account | address |
return | type |
---|---|
unit |
Allowance - MUST trigger on any successful allowance creation or change.
The approval event arguments should be as follows: (from_account, for_account, value)
Allowance(address, address, int)
parameter | type |
---|---|
from_account | address |
for_account | address |
value | int |
This function burns the whole balance of the Call.caller
and stores the same amount in the swapped
map.
stateful entrypoint swap() : unit
parameter | type |
---|---|
value | int |
return | type |
---|---|
() | unit |
This function returns the amount of tokens that were burned trough swap
for the provided account.
stateful entrypoint check_swap(account: address) : int
parameter | type |
---|---|
account | address |
return | type |
---|---|
int | int |
This function returns all swapped tokens that are stored in contract state.
stateful entrypoint swapped() : map(address, int)
return | type |
---|---|
swapped | map(address, int) |
Swap - MUST trigger when tokens are swapped and thus are no longer available in the given token contact.
The swap event arguments should be as follows: (account, value)
Swap(address, int)
parameter | type |
---|---|
account | address |
value | int |
There are several implementations available at the moment, but they lack a thing or two (that is why this standard is being proposed).
Example implementations: