Smart Contracts
CommunityManagement

CommunityManagement

CommunityManagement.aes holds everything about a community that is policy rather than economics: who the moderators are, who is muted, how many tokens you need to hold to write, and the community's free-form metadata.

One instance is deployed per community by the factory. Its owner is the community's DAO, so every setting on it changes only through a DAO vote, except muting, which moderators do directly.

Resolve it with factory.get_community_management(token_sale). Its interface is inlined in CommunityFactory.aci.json.

Entrypoints

EntrypointSignatureFlags
init(initial_moderator: address, minimum_token_threshold: int, is_private: bool, meta_info: CommunityManagement.meta_info) => statestateful
add_moderator(moderator: address) => unitstateful
delete_moderator(moderator: address) => unitstateful
mute_user_id(user_id: string) => unitstateful
unmute_user_id(user_id: string) => unitstateful
change_minimum_token_threshold(minimum_token_threshold: int) => unitstateful
change_meta_info(meta_info: CommunityManagement.meta_info) => unitstateful
set_owner(owner: address) => unitstateful
moderator_accounts() => Set.set(address)
muted_user_ids() => Set.set(string)
minimum_token_threshold() => int
meta_info() => CommunityManagement.meta_info
owner() => address
version() => int
get_state() => state

Types

type meta_info = map(string, string)

record state =
  { owner                   : address
  , minimum_token_threshold : int
  , is_private              : bool
  , moderator_accounts      : Set.set(address)
  , muted_user_ids          : Set.set(string)
  , meta_info               : meta_info }

Set.set('a) is the æternity standard library set, a record wrapping a map:

record set('a) = { to_map : map('a, unit) }

Decoded in JavaScript the SDK gives you a Set, so membership is a plain .has():

const moderators = await cm.moderator_accounts().then((r) => r.decodedResult);
moderators.has('ak_alice...');   // true or false

The meta_info map

meta_info is map(string, string) with no schema, no validation and no reserved keys. The Superhero app writes these keys when it creates a community:

KeyWhat the app puts thereWhere it shows up
collectionthe collection slug, e.g. wordinternal
descriptionfree textthe token page, truncated to 150 characters in the UI and 160 for the page meta description
websitea URLthe token page
twitteran X handle or URLthe token page

All four are optional. A community created with new Map() has no metadata, and the app falls back to a generated description.

change_meta_info replaces the whole map. There is no per-key update, so read the current map, modify it, and write it back through a ChangeMetaInfo(map(string, string)) DAO vote:

const current = await cm.meta_info().then((r) => r.decodedResult);
current.set('description', 'A new description');
// change_meta_info is owner-gated: submit `current` as a ChangeMetaInfo vote subject.

Events

datatype event =
    ChangeMinimumTokenThreshold(int)
  | AddModerator(address)
  | DeleteModerator(address)
  | MuteUserId(string)
  | UnmuteUserId(string)
  | SetOwner(address)
  | ChangedMetaInfo

ChangedMetaInfo is the only event in the Superhero suite that carries no arguments. It signals that the map changed without saying how. An indexer that wants the new metadata re-reads meta_info() when it sees this event.

Who may call what

ActionCaller
add_moderator, delete_moderatorthe owner (the DAO), via an AddModerator / DeleteModerator vote
change_minimum_token_thresholdthe owner, via a ChangeMinimumTokenThreshold vote
change_meta_infothe owner, via a ChangeMetaInfo vote
set_ownerthe owner; this is how a ChangeDAO migration hands over control
mute_user_id, unmute_user_ida moderator, directly

Two guard functions enforce that table:

function require_owner() =
  require(Call.caller == state.owner, "ONLY_OWNER_CAN_CHANGE")

function require_moderator() =
  require(Set.member(Call.caller, state.moderator_accounts), "ONLY_MODERATOR_CAN_CHANGE")

Moderators are not owners: a moderator cannot add another moderator, change the threshold, or touch the metadata. The owner is not automatically a moderator either, so the DAO cannot mute anyone without first voting itself onto the moderator set. The initial moderator is the community creator, set once in init; the owner becomes the DAO immediately afterwards.

Muting is the one action that does not need a vote: governance sets the policy and appoints the people, and those people act in real time.

Initial values at creation

The factory deploys community management with fixed initial settings:

Chain.create(Call.caller, 10 ^ decimals(), is_private, meta_info) : CommunityManagement
FieldInitial value
initial_moderatorCall.caller, whoever created the community
minimum_token_threshold10 ^ 18, i.e. exactly one whole token
is_privatewhatever you passed to create_community
meta_infowhatever you passed to create_community
ownerCall.caller at construction, then the DAO

The default threshold is a literal in CommunityFactory.create_community, not a parameter, so every community starts at one whole token. Only a ChangeMinimumTokenThreshold vote moves it.

What minimum_token_threshold gates

On chain, it gates proposal creation. DAO.add_vote calls community_management.minimum_token_threshold() and requires the proposer's balance to meet it, aborting with ADD_VOTE_TOKEN_HOLDER_MISSMATCH otherwise. No other contract reads it.

Off chain, it gates the community's chat room. The Superhero API mirrors the on-chain state and uses the threshold as the membership predicate for the community's token-gated NIP-29 room: a holder is eligible when their raw balance is at least the raw threshold (base units compared to base units, no decimal shifting) and their address is not muted. A muted holder is ineligible however many tokens they hold.

What a user_id is

mute_user_id and unmute_user_id take a string, not an address, and the contract places no constraint on the format. Superhero's chat layer compares entries from muted_user_ids against æternity account addresses, so write ak_... addresses if you want a mute to take effect in Superhero. A client reading the list should still handle other strings rather than assuming the prefix.

What is_private controls

On chain, is_private is set once in init and not read by any entrypoint. It has no dedicated getter; read it through get_state(). Contract state is public, so a private community's moderators, mutes, threshold and metadata are readable by anyone.

Off chain, the Superhero API copies it to the community's chat room and uses it to choose the visibility tag on the NIP-29 edit-metadata event, public or private. The web client reports a private room as auth-required rather than rendering it openly. A token with no chat room defaults to public.

is_private is a chat-room visibility preference stored on chain, not a confidentiality control over on-chain data.

Related

  • DAO: the owner of this contract and the source of every governed change
  • CommunityFactory: where the initial values above are set
  • Moderators: the moderator role explained for community members
  • Moderation: what muting does in the social layer
  • Proposals: the vote subjects that change these settings