CommunityFactory
CommunityFactory.aes is the entry point to the whole token system. It holds the registry of
collections, deploys the four contracts that make up a community in a single transaction,
mints ProtocolDAO tokens on every buy, and is the canonical place to look up the shared
bonding curve and affiliation treasury addresses.
It is deployed once per network and reused for every collection and community after that.
| Network | Address |
|---|---|
| Mainnet | ct_25cqTw85wkF5cbcozmHHUCuybnfH9WaRZXSgEcNNXG9LsCJWTN |
| Testnet | ct_vLKrYRCthfViqUuWFKGYgz7kxhvrsdAoKhZPXqzxcaEFRkZy1 |
CommunityFactory.aci.json also carries the inlined interfaces of DAO, DAOVote,
CommunityManagement, AffiliationTreasury, VoteSubjects, Frac and FungibleTokenFull.
Four values are fixed, so you never need a node to know them:
| Call | Returns | Note |
|---|---|---|
factory_type() | "AFFILIATION_COMMUNITY_FACTORY" | literal |
version() | 1 | literal |
fee_percentage() | 500 | passthrough to AffiliationTreasury.get_affiliation_fee_percentage() |
fee_precision() | 100000 | passthrough to AffiliationTreasury.get_affiliation_fee_precision() |
fee_percentage() / fee_precision() is 500 / 100000, or 0.5%. Divide the two rather than
hardcoding 0.005; both are passthroughs and a treasury redeployment could move them.
Entrypoints
| Entrypoint | Signature | Flags |
|---|---|---|
init | (protocol_token_name: string) => CommunityFactory.state | stateful |
create_collection | (collection_name: string, allowed_name_length: int, allowed_name_chars: list(CommunityFactory.allowedNameChars)) => string | stateful, payable |
create_community | (collection_name: string, name: string, initial_buy_count: int, is_private: bool, meta_info: CommunityManagement.meta_info) => tuple(DAO, TokenSale, CommunityManagement) | stateful, payable |
mint_protocol_dao_token | (buy_value: int, receiver: address) => unit | stateful |
has_community | (collection_name: string, name: string) => bool | |
get_token_sale | (collection_name: string, name: string) => TokenSale | |
token_contract | (collection_name: string, name: string) => FungibleTokenFull | |
get_community_management | (token_sale: TokenSale) => CommunityManagement | |
token_sale_registry | (collection_name: string) => CommunityFactory.token_sale_registry | |
community_management | () => CommunityFactory.community_management | |
allowed_name_length | (collection_name: string) => int | |
allowed_name_chars | (collection_name: string) => list(CommunityFactory.allowedNameChars) | |
bonding_curve | () => BondingCurve | |
affiliation_treasury | () => AffiliationTreasury | |
protocol_dao_token | () => FungibleTokenFull | |
fee_percentage | () => int | |
fee_precision | () => int | |
factory_type | () => string | |
version | () => int | |
get_state | () => CommunityFactory.state |
Types
type token_sale_registry = map(string, TokenSale)
type known_token_sales = Set.set(address)
type community_management = map(TokenSale, CommunityManagement)
type collection_registry = map(string, collection_data)
datatype allowedNameChars =
SingleChar(int) // one allowed Unicode code point
| CharRangeFromTo(int, int) // an inclusive code point range
record collection_data =
{ allowed_name_chars : list(allowedNameChars)
, allowed_name_length : int
, token_sale_registry : token_sale_registry }
record state =
{ collection_registry : collection_registry
, known_token_sales : known_token_sales
, community_management : community_management
, affiliation_treasury : AffiliationTreasury
, bonding_curve : BondingCurve
, protocol_dao_token : FungibleTokenFull }allowedNameChars values are Unicode code points as integers, not characters.
CharRangeFromTo(65, 90) is A-Z; CharRangeFromTo(48, 57) is 0-9; SingleChar(45) is a
single dash. That is how the global WORDS collection expresses "uppercase A-Z, digits, and
dash". Decoded in JavaScript, each entry is a one-key object:
const chars = await factory.allowed_name_chars(collectionId).then((r) => r.decodedResult);
// [ { CharRangeFromTo: [ 65n, 90n ] }, { CharRangeFromTo: [ 48n, 57n ] }, { SingleChar: [ 45n ] } ]CommunityManagement.meta_info is map(string, string): free-form key/value metadata with
no schema enforced on chain. The Superhero app writes four keys when it creates a token:
| Key | Value |
|---|---|
collection | the collection slug, e.g. word |
description | free text, rendered on the token page |
website | a URL |
twitter | an X handle or URL |
All four are optional and the contract validates none of them. Pass a Map, not an
object. An empty new Map() is valid.
Events
datatype event =
CreateCommunity(string, DAO, TokenSale, CommunityManagement)
// name, dao, sale, community management
| CreateCollection(string, address)
// collection id, creatorCreateCommunity is the single best source for the three per-community addresses. Capture
it instead of making three follow-up reads.
Creating a collection
create_collection registers a naming namespace.
It costs exactly 555 AE, and the AE is burned. The contract requires
Call.value == 555 * (10 ^ 18), aborts with COLLECTION_CREATION_DONATION_MISMATCH
otherwise, then spends the full amount to ak_11111111111111111111111111111111273Yts, the
æternity burn address.
The stored collection id is not the name you passed. The contract concatenates the name, a dash, and the caller's address:
create_collection("WORDS", ...) called by ak_alice...
→ stored as "WORDS-ak_alice..."That returned string is the id every later call must use, and it is why collection ids in the
Superhero API are typed as ${string}-${Encoded.AccountAddress}. Creating a collection whose
name already exists aborts with collection_ALREADY_EXISTS (note the lowercase first letter).
Any account may register a collection, with any naming rules. The collections the Superhero
app lists on mainnet (WORDS, CHINESE, ARABIC and RUSSIAN) are a curated set
maintained in the API.
Creating a community
create_community is the transaction that brings a token into existence. In one call it:
- Looks up the collection (
COLLECTION_NOT_FOUND), checks the name is unused (NAME_ALREADY_REGISTERED), and validates it against the collection's length (NAME_TOO_LONG) and character rules (NAME_NOT_ALLOWED_CHAR). - Deploys a
CommunityManagementcontract with the caller as the initial moderator, a minimum token threshold of10 ^ 18(one whole token), and youris_privateandmeta_info. - Deploys a
DAOwith no token sale set, pointing back at the factory. - Deploys an
AffiliationBondingCurveTokenSale, which in turn deploys the community's AEX-9 token, pointing at the shared bonding curve and affiliation treasury. Thenameargument is used as both the token's name and its symbol. - Calls
dao.set_token_sale(sale)andcommunity_management.set_owner(dao), closing the circle that neither constructor could close alone. - Optionally executes an initial buy of
initial_buy_counttokens with the attached AE. - Registers the sale in the collection's
token_sale_registry, inknown_token_sales, and in thecommunity_managementmap. - Emits
CreateCommunity.
It returns the three addresses as a tuple, decoded in JavaScript as an array:
const result = await factory.create_community(
collectionId,
'MYTOKEN',
initialBuyTokenDecimals, // string, in token base units
false, // is_private
new Map(), // meta_info
{ amount: initialBuyPriceAetto },
);
const [daoAddress, tokenSaleAddress, communityManagementAddress] = result.decodedResult;What initial_buy_count is denominated in
initial_buy_count is a token count in base units, not an AE amount. Community tokens use
18 decimals, so buying 5 whole tokens means passing 5 * 10^18.
To go the other way ("I want to spend 0.1 AE"), invert the bonding curve in two steps:
// 1. Estimate a token count from an AE amount with client-side curve math.
const tokenCount = calculateTokensFromAE(buyAe); // numeric integration
const decimals = await bondingCurve.supported_decimals().then((r) => r.decodedResult);
const initialBuyTokenDecimals = tokenCount
.multipliedBy(new BigNumber(10).pow(Number(decimals)))
.toFixed(0);
// 2. Ask the chain for the exact price of that count, then add a buffer.
const onChainPrice = await bondingCurve
.calculate_buy_price(0n, BigInt(initialBuyTokenDecimals))
.then((r) => r.decodedResult);
const amount = new BigNumber(onChainPrice.toString())
.multipliedBy(1.05) // 5% buffer
.toFixed(0, BigNumber.ROUND_CEIL);The client-side estimate only picks a count; the chain always sets the price. Pass 0n as
the supply to calculate_buy_price because a brand-new community starts at zero supply.
Overpayment is refunded by the sale contract, so the buffer is safe.
Pass initial_buy_count as 0 and attach no AE to create a token with zero supply.
Minting ProtocolDAO tokens
mint_protocol_dao_token(buy_value, receiver) is called by a sale contract during every buy,
not by users. The factory checks the caller:
require(Set.member(Call.caller, state.known_token_sales), "NOT_KNOWN_TOKEN_SALE")
state.protocol_dao_token.mint(receiver, buy_value * protocol_dao_token_multiplier)Only sales the factory itself deployed are in known_token_sales, so no other contract can
mint protocol tokens.
The multiplier is a literal in the contract:
// mints protocol DAO tokens at a rate of 1000 times the buy value.
let protocol_dao_token_multiplier = 1000A 1 AE buy mints 1,000 ProtocolDAO tokens to the buyer. The base is the net AE the buyer
paid (Call.value minus any overpayment refund), which includes the affiliation fee as well
as the curve price.
An initial buy at creation also mints them. The sale skips its callback when the caller is
the factory, and create_community performs the mint itself instead:
bonding_curve_token_sale.buy_with_affiliation(value = Call.value - overpaid_refund,
initial_buy_count, Call.caller)
bonding_curve_token_sale.token_contract().transfer(Call.caller, initial_buy_count)
state.protocol_dao_token.mint(Call.caller, (Call.value - overpaid_refund) * protocol_dao_token_multiplier)The transfer on the middle line exists for the same reason: the sale mints community tokens
to its Call.caller, which during creation is the factory, so the factory forwards them to
you. A creator doing an initial buy receives the tokens and the ProtocolDAO tokens exactly as
an ordinary buyer would.
The factory refunds overpayment before the buy, against
bonding_curve_token_sale.price(initial_buy_count), and aborts with
VALUE_HAS_TO_BE_GREATER_THAN_BUY_PRICE if you attach less than that.
Related
- AffiliationBondingCurveTokenSale: the sale contract this factory deploys
- CommunityManagement: the
meta_infomap and moderator model - Collections and naming: the naming rules from a user's point of view
- Create a token from the CLI: a working end-to-end creation flow
- Deployed Addresses: factory addresses and runtime resolution