Moderation
Content on Superhero cannot be removed. The Tipping contract has no delete and no update
entrypoint, so every post and comment ever published stays in the contract's state. Moderation
happens at the community application layer, through token-holder governance, and what it
controls is access to a community's chat room and the threshold for opening a DAO vote.
CommunityManagement.aes
Each community gets its own CommunityManagement contract, deployed by the factory during
community creation.
State
| Field | Type | Meaning |
|---|---|---|
owner | address | The contract owner; set to Call.caller at init, transferable with set_owner |
minimum_token_threshold | int | Raw base-unit token balance required for write access in this community |
is_private | bool | Whether the community is private |
moderator_accounts | set of address | The current moderator set |
muted_user_ids | set of string | Muted identifiers: ak_… addresses held as strings |
meta_info | map of string to string | Community metadata |
Entrypoints and who may call them
Two guards cover every write:
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")| Entrypoint | Signature | Guard |
|---|---|---|
get_state | () => CommunityManagement.state | read |
moderator_accounts | () => Set.set(address) | read |
muted_user_ids | () => Set.set(string) | read |
minimum_token_threshold | () => int | read |
owner | () => address | read |
meta_info | () => meta_info | read |
version | () => int | read; returns 1 |
mute_user_id | (user_id: string) => unit | require_moderator |
unmute_user_id | (user_id: string) => unit | require_moderator |
add_moderator | (moderator: address) => unit | require_owner |
delete_moderator | (moderator: address) => unit | require_owner |
change_minimum_token_threshold | (minimum_token_threshold: int) => unit | require_owner |
change_meta_info | (meta_info: meta_info) => unit | require_owner |
set_owner | (owner: address) => unit | require_owner |
Every write emits a typed event first, then applies: MuteUserId(string),
UnmuteUserId(string), AddModerator(address), DeleteModerator(address),
ChangeMinimumTokenThreshold(int), SetOwner(address) and ChangedMetaInfo.
The contract checks the owner, not the DAO; the DAO is whoever the owner is.
Who owns it, and who the first moderator is
CommunityFactory.create_community settles both in one transaction:
let community_management = Chain.create(Call.caller, 10 ^ decimals(), is_private, meta_info) : CommunityManagement
let dao = Chain.create(None, Contract.address) : DAO
...
community_management.set_owner(dao.address)| Question | Answer |
|---|---|
| Initial owner | The factory, because init sets owner = Call.caller, then reassigned to the DAO in the same transaction by set_owner |
| Final owner | The community's DAO contract |
| Initial moderator | The account that called create_community, the community's creator |
Initial minimum_token_threshold | 10 ^ decimals() with decimals() = 18, so one whole token in raw base units |
Initial muted_user_ids | Empty (Set.new()) |
is_private | Exactly the boolean passed to create_community |
The creator gets the first moderator seat, and the DAO gets everything else.
Changing the owner
set_owner requires the current owner, which is the DAO. The DAO exposes one route to it: the
ChangeDAO vote subject, which moves the token sale's beneficiary and owner to a replacement
DAO and then calls community_management.set_owner(new_dao_address). It first requires that
the replacement references the same CommunityManagement instance
(NEW_DAO_HAS_TO_REFERENCE_SAME_COMMUNITY_MANAGEMENT), so a ChangeDAO vote cannot point a
community at somebody else's moderation contract.
Who can do what
| Action | Who | How |
|---|---|---|
| Mute a user id | A moderator | A direct call to mute_user_id |
| Unmute | A moderator | unmute_user_id |
| Appoint a moderator | Token holders | An AddModerator DAO vote, applied to the contract |
| Remove a moderator | Token holders | A DeleteModerator DAO vote |
| Change the write threshold | Token holders | A ChangeMinimumTokenThreshold DAO vote |
| Change community metadata | Token holders | A ChangeMetaInfo DAO vote |
| Replace the owner | Token holders | A ChangeDAO vote, which reassigns set_owner |
Moderators act unilaterally within their mandate; the mandate itself is granted and revoked only by a vote.
The vote that appoints a moderator
Moderation changes ride the same DAO machinery as treasury payouts:
| Rule | Value |
|---|---|
| Who may open a vote | A holder with at least the community threshold of tokens |
| Cost to open | 555 AE, paid to the DAO |
| Voting period | Two weeks, measured in key-blocks |
| Approval required | At least 55% of the vote stake in favour |
| Quorum | At least 10% of the total token stake participating |
| Execution window | Roughly two days after the vote closes |
| Who executes | Anyone. Any account or script can trigger the application |
Tokens used to vote are locked by the vote contract via the AEX-9 allowance mechanism and can
be reclaimed with revoke_vote before the close or withdraw after it. The 555 AE goes to the
DAO whether or not the vote passes.
Appointing a moderator therefore takes two weeks and 555 AE.
The app offers five of the seven vote subjects in its create-vote UI: payout, change DAO, change the minimum token threshold, add moderator, delete moderator. The two it omits need richer input than the form supports: a payout of a specific amount takes two fields, and changing the metadata takes a map.
What a mute does
A user_id is the holder's ak_… account address held as a string. The token-gated-rooms
pipeline enforces it:
- The API reads
CommunityManagement.get_state()and mirrorsmuted_user_idsinto the room's muted list, alongsidemoderators,min_token_threshold,is_privateand the owner address. The relay's admin and role lists are derived from the same read. - Eligibility for the room is
balance >= min_token_threshold AND NOT muted, where the muted check compares the member's æternity address against that list. - A muted member is removed from the relay group with a kind-9001 event, and the sync refuses to publish a kind-9000 add for any member currently muted, so a mute is not undone by a later re-sync.
Mute a holder and they become ineligible for that community's chat room regardless of balance, until the mute is lifted.
A mute affects chat-room eligibility only. Posts, comments, topic feeds and the popular ranking
are not filtered by muted_user_ids: a muted account's existing posts stay visible, and it can
keep posting.
The per-room mute in the chat app is a different thing: it is a notification preference that silences your own alerts and changes nothing for anyone else. See Notifications.
The write threshold
minimum_token_threshold is a raw base-unit token balance, and it gates two things:
| Gate | Where | Rule |
|---|---|---|
| Opening a DAO vote | DAO.add_vote | token.balance(Call.caller) >= minimum_token_threshold, else ADD_VOTE_TOKEN_HOLDER_MISSMATCH |
| Entering the community chat room | The rooms eligibility check | raw balance >= raw threshold, and not muted |
It starts at one whole token (10 ^ 18) and moves only by a ChangeMinimumTokenThreshold
vote. The same value surfaces in the chat rooms API as min_token_threshold, alongside the
room's is_private flag. A room a holder is not eligible for does not appear in their room
list, and the composer shows a "hold the token" gate. See Chat.
The privacy flag
create_community(collection_name, name, initial_buy_count, is_private, meta_info) passes
is_private straight into CommunityManagement's init, and the rooms service reads it back
out of get_state() unchanged. The community's on-chain privacy flag and the chat room's are
the same value, chosen once at creation.
What moderation cannot do
- It cannot delete a post. The bytes stay in contract state.
- It cannot delete a comment. Comments are posts.
- It cannot prevent republication. Anyone can post the same content again.
- It does not hide anything the muted account posted. A mute is a chat-room exclusion, not a content filter.
- It does not reach other clients. A different frontend reading the same contract is free to ignore every mute; a different relay would enforce nothing.
- It is per-community. There is no global mute list; each
CommunityManagementinstance holds its own.
Hiding your own post
The only post-visibility flag is is_hidden, and the indexer sets it from the post's own media
list. Any entry whose text contains hidden marks the post hidden at index time, which removes
it from the feeds and from popular ranking. It is a convention an author applies to their own
post, not a moderator action, and it does not remove the post from the chain.
Related
- Posting: why there is nothing to delete in the first place.
- Chat: the relay-side membership control, the other half of community moderation.
- Moderators: the governance view of appointing and removing them.
- Voting: the mechanics, quorum and thresholds behind every moderation change.
- Community management contract: the contract reference.