FungibleTokenFull (AEX-9)
FungibleTokenFull.aes is the AEX-9 implementation every Superhero token uses, for
every community's token and for the ProtocolDAO token. It is the æternity reference fungible token
(aeternity-fungible-token v2.2.0) with the allowance, mintable, burnable and swappable
extensions.
There is no single address. Resolve a community token with
factory.token_contract(collection_name, name) or sale.token_contract(). The ProtocolDAO
token is ct_dsa6octVEHPcm7wRszK6VAjPp1FTqMWa7sBFdxQ9jBT35j6VW on mainnet and
ct_2cyV58CrBwi2k4kvP3mN517C1NH21zfxxYmaAyb41GXdXSsRvN on testnet, or call
factory.protocol_dao_token().
Entrypoints
| Entrypoint | Signature | Flags |
|---|---|---|
init | (name: string, decimals: int, symbol: string, initial_owner_balance: option(int)) => state | |
transfer | (to_account: address, value: int) => unit | stateful |
transfer_allowance | (from_account: address, to_account: address, value: int) => unit | stateful |
create_allowance | (for_account: address, value: int) => unit | stateful |
change_allowance | (for_account: address, value_change: int) => unit | stateful |
reset_allowance | (for_account: address) => unit | stateful |
mint | (account: address, value: int) => unit | stateful |
burn | (value: int) => unit | stateful |
balance | (account: address) => option(int) | |
balances | () => FungibleTokenFull.balances | |
allowance | (allowance_accounts: FungibleTokenFull.allowance_accounts) => option(int) | |
allowance_for_caller | (from_account: address) => option(int) | |
allowances | () => FungibleTokenFull.allowances | |
meta_info | () => FungibleTokenFull.meta_info | |
total_supply | () => int | |
owner | () => address | |
aex9_extensions | () => list(string) | |
swap | () => unit | stateful |
swapped | () => map(address, int) | |
check_swap | (account: address) => int | stateful |
Types
record meta_info =
{ name : string
, symbol : string
, decimals : int }
record allowance_accounts =
{ from_account : address // the owner of the tokens
, for_account : address } // the spender
type balances = map(address, int)
type allowances = map(allowance_accounts, int)
record state =
{ owner : address
, total_supply : int
, balances : balances
, meta_info : meta_info
, allowances : allowances
, swapped : map(address, int) }allowance takes the allowance_accounts record as a single argument, not as two
addresses. Pass an object with exactly those two field names:
const current = await token
.allowance({ from_account: 'ak_owner...', for_account: 'ak_spender...' })
.then((r) => r.decodedResult);Events
datatype event =
Transfer(address, address, int) // from, to, value
| Allowance(address, address, int) // owner, spender, value
| Burn(address, int) // account, value
| Mint(address, int) // account, value
| Swap(address, int) // account, valueThese are the standard AEX-9 events. A single token buy produces a Mint on the community
token and a Mint on the ProtocolDAO token in the same transaction. See
Events for how to tell them apart.
Decimals
Community tokens have 18 decimals: one whole token is 1000000000000000000 base units,
the same scale as AE itself. Every value, count and amount in these contracts is in base
units.
The factory passes 18 to every token it creates, and the sale contract requires the bonding
curve to be calibrated for the same value:
require(decimals == bonding_curve.supported_decimals(),
"BONDING_CURVE_SUPPORTED_DECIMALS_MISSMATCH")Read meta_info() and use its decimals field rather than hardcoding 18. The ProtocolDAO
token or a future collection could differ:
const meta = await token.meta_info().then((r) => r.decodedResult);
const whole = new BigNumber(rawBalance.toString()).shiftedBy(-Number(meta.decimals));How a community token is created
The sale contract creates the token in its own init:
token_contract = Chain.create(name, decimals, symbol, None) : FungibleTokenFullBecause Chain.create runs inside the sale contract, the sale contract is the token's
owner, and that is what authorises mint on a buy and burn on a sell.
initial_owner_balance is None, so a new community token starts at zero total supply: every
token in existence was minted by a buy. The name and the symbol are the same string, because
CommunityFactory.create_community passes its name argument to both positions.
Allowances
Two Superhero flows require an allowance before they work:
| Flow | Grant the allowance to |
|---|---|
| Selling tokens back to the bonding curve | the sale contract address, ct_ rewritten to ak_ |
| Voting in a community DAO | the vote contract address, ct_ rewritten to ak_ |
The ct_ to ak_ rewrite is required. A contract's account identity uses the ak_ prefix
over the same key bytes, and that is the address that appears as for_account:
const forAccount = saleAddress.replaceAll('ct_', 'ak_');Three entrypoints manage the value:
| Entrypoint | Semantics | When to use it |
|---|---|---|
create_allowance(for_account, value) | Creates a new allowance entry. | First time granting to this spender |
change_allowance(for_account, value_change) | Adds a delta to an existing allowance. The delta may be negative. | Adjusting an existing allowance |
reset_allowance(for_account) | Sets it to zero. | Revoking |
The pattern:
const existing = await token
.allowance({ from_account: aeSdk.address, for_account: forAccount })
.then((r) => r.decodedResult)
.catch(() => undefined);
if (existing === undefined) {
await token.create_allowance(forAccount, amount);
} else {
const delta = new BigNumber(amount).minus(existing.toString()).toFixed();
await token.change_allowance(forAccount, delta);
}allowance returns option(int), so a never-granted allowance decodes to undefined. The
.catch(() => undefined) covers a call that aborts on a missing entry rather than returning
None.
allowance_for_caller(from_account) is the same read from the spender's side.
Mint and burn
mint and burn are how supply tracks the bonding curve:
- On a buy, the sale contract calls
mint(buyer, count)and supply goes up. - On a sell, the sale contract pulls your tokens with
transfer_allowanceand callsburn, and supply goes down.
Both are owner-gated. For a community token the owner is the sale contract, so the only way to
increase supply is to pay the curve. That is what makes total_supply() a reliable input to a
price calculation.
The swap extension
swap, swapped and check_swap come from the AEX-9 swappable extension, which migrates
balances from an older token contract into a newer one: swap() credits the caller based on
their balance in the predecessor and records the amount in the swapped map. Superhero does
not use this path; for a factory-created token swapped() is empty.
aex9_extensions() returns the list of extension names the contract implements. It is the
standard's own discovery mechanism, so call it instead of assuming.
Reading balances
| Read | Returns | Note |
|---|---|---|
balance(account) | option(int) | undefined when the account has never held the token, not zero |
balances() | map(address, int) | Every holder. Large on popular tokens; prefer the middleware |
total_supply() | int | The input to every bonding curve price call |
For holder lists and balances at scale, query the middleware's AEX-9 endpoints
(/v3/aex9/<contract address>) rather than calling balances(), which returns the entire map
in one response.
Related
- AffiliationBondingCurveTokenSale: the contract that mints and burns these tokens
- DAOVote: the other flow that needs an allowance
- Calling Contracts: SDK setup and the full allowance example
- AEX-9 tokens in your wallet: the same token from a holder's point of view
- AEX standards: where AEX-9 sits among the æternity standards
- AEX-9 specification (opens in a new tab): the standard itself