Buzzer
Title: Buzzer — a fully on-chain, permissionless social protocol
Author: Buzzer
Single immutable contract with no owner, no upgrade path and no tunable parameters. It holds identity (profiles, linked keys, recovery), unique names, the social graph, profile data, posts ("buzzes"), reactions with ERC-20 tips, and communities.
Design principles
- Gas first. Posts and reactions are events only (no per-post storage). Every hot path performs exactly one authorisation SLOAD.
- Validate only what protects funds or control. Anything that merely affects UX (reply targets, reaction types, content encoding, reserved ranges) is a client convention.
- Explicit actor model. Every action names the acting
uid; the caller must hold the relevant permission bit for that uid. Addresses act for profiles, they do not own them. - Consent everywhere. Nothing (custody, a community, a linked key) can be pushed onto an address or profile without an accept step or a signature from the receiver.
- Money never gets stuck. Fees and tip shares are pushed to
TREASURYimmediately; if a push fails the funds park here and anyone cansweepthem later.
Roles per profile
- custody : exactly one address; owns the uid; holds every permission (
PERM_ALL). - recovery : optional guardian that can move custody after a 7-day delay.
- linked : any number of addresses, each with its own permission bitmap.
Gasless usage: any function can be executed on behalf of a signer through
executeSigned(EIP-712, ECDSA or ERC-1271). Many actions can be batched withmulticall. Spec of record: specs/spec.md (v1.7). Client conventions: docs/conventions.md.
State Variables
TREASURY
Recipient of every protocol fee and tip share (a BuzzerTreasury contract). Immutable.
address public immutable TREASURY
INVITE_SIGNER
Genesis invite gate: the key whose EIP-712 Invite vouchers admit the first users.
address(0) disables the gate entirely (open from block zero). Immutable — the gate can
only ever loosen or expire, never be re-closed or re-keyed.
address public immutable INVITE_SIGNER
INVITE_OPEN_TIME
Timestamp at which the invite gate auto-opens regardless of user count (deploy + INVITE_PERIOD).
0 when INVITE_SIGNER is unset. A lost signer key can never brick onboarding past this time.
uint256 public immutable INVITE_OPEN_TIME
PROFILE_FEE
Fee (in wei) to create a profile.
uint256 public constant PROFILE_FEE = 0.0004 ether
COMMUNITY_FEE
Fee (in wei) to create a community.
uint256 public constant COMMUNITY_FEE = 0.001 ether
INVITE_CUTOFF
Genesis invite gate closes once this many profiles exist (uids 1..INVITE_CUTOFF are gated).
uint256 public constant INVITE_CUTOFF = 1000
INVITE_PERIOD
Genesis invite gate auto-opens this long after deployment even if INVITE_CUTOFF isn't reached.
uint256 public constant INVITE_PERIOD = 20 days
NAME_CHANGE_INTERVAL
Minimum time between two name changes (buzz names and community names alike).
uint256 public constant NAME_CHANGE_INTERVAL = 30 days
RECOVERY_DELAY
Delay between startRecovery and executeRecovery.
uint256 public constant RECOVERY_DELAY = 7 days
LINK_PROPOSAL_TTL
A link proposal (proposeLink) can be accepted for this long.
uint256 public constant LINK_PROPOSAL_TTL = 7 days
NAME_MAX_LEN
Maximum handle length. Charset is [a-z0-9]. A name is a right-padded bytes32, so up to
32 chars fit with no storage change; the cap is a policy choice.
uint256 public constant NAME_MAX_LEN = 20
PERM_VERIFIED
The address is a "login" wallet of the profile. Grants no on-chain power by itself.
uint256 public constant PERM_VERIFIED = 1 << 0
PERM_POST
May call buzz, communityBuzz, deleteBuzz.
uint256 public constant PERM_POST = 1 << 1
PERM_REACT
May call react (tips are pulled from the acting address).
uint256 public constant PERM_REACT = 1 << 2
PERM_GRAPH
May call setRelationships, setMemberEdge.
uint256 public constant PERM_GRAPH = 1 << 3
PERM_PROFILE
May call setProfileData.
uint256 public constant PERM_PROFILE = 1 << 4
PERM_COMMUNITY
May create communities and act as the profile's owner/admin/member on communities.
uint256 public constant PERM_COMMUNITY = 1 << 5
PERM_MESSAGE
May call sendMessage (the on-chain messaging tier), separate from PERM_POST.
uint256 public constant PERM_MESSAGE = 1 << 6
PERM_ALL
Sentinel permission word held only by custody. Linked keys are clamped to PERM_ALL >> 1.
uint256 internal constant PERM_ALL = type(uint256).max
STANDING_MEMBER
The uid is a member of the community.
uint256 public constant STANDING_MEMBER = 1 << 0
STANDING_ADMIN
The uid may write other members' standing (see setStanding). Contract-honoured.
uint256 public constant STANDING_ADMIN = 1 << 1
STANDING_BANNED
The uid is banned. Advisory: clients hide its posts; the chain still records them.
uint256 public constant STANDING_BANNED = 1 << 2
STANDING_CAN_POST
The uid is an approved poster. Advisory.
uint256 public constant STANDING_CAN_POST = 1 << 3
STANDING_MODERATOR
The uid is a moderator. Advisory.
uint256 public constant STANDING_MODERATOR = 1 << 4
STANDING_VERIFIED
The uid is verified by the community. Advisory.
uint256 public constant STANDING_VERIFIED = 1 << 5
DOMAIN_SEPARATOR
EIP-712 domain separator: {name:"Buzzer", version:"1", chainId, verifyingContract}.
bytes32 public immutable DOMAIN_SEPARATOR
EXECUTE_TYPEHASH
Execute(address signer,bytes data,uint256 nonce,uint256 deadline)
bytes32 internal constant EXECUTE_TYPEHASH =
keccak256("Execute(address signer,bytes data,uint256 nonce,uint256 deadline)")
LINK_CONSENT_TYPEHASH
LinkConsent(uint256 uid,address addr,address custody,uint256 perms,uint256 nonce,uint256 deadline)
bytes32 internal constant LINK_CONSENT_TYPEHASH =
keccak256("LinkConsent(uint256 uid,address addr,address custody,uint256 perms,uint256 nonce,uint256 deadline)")
INVITE_ROOT_TYPEHASH
InviteRoot(bytes32 root,uint256 deadline) — the genesis authorization: INVITE_SIGNER (a 2-of-6
threshold signer) signs the Merkle root of a pool of invite-code leaves once. A redeemer proves its
code is in the signed root (see _consumeInvite). Codes are bearer tokens; see docs/invite-gate.md.
bytes32 internal constant INVITE_ROOT_TYPEHASH = keccak256("InviteRoot(bytes32 root,uint256 deadline)")
ERC1271_MAGIC
ERC-1271 isValidSignature(bytes32,bytes) selector and magic return value.
bytes4 internal constant ERC1271_MAGIC = 0x1626ba7e
SENDER_TSLOT
Transient-storage slot holding the meta-tx actor while executeSigned is live. ("buzzer.sender")
uint256 internal constant SENDER_TSLOT = 0x706f6e732e62757a7a2e73656e646572
uidCounter
Last assigned profile id. Ids start at 1 and are never reused.
uint256 public uidCounter
bidCounter
Last assigned post id. Ids start at 1 and are never reused.
uint256 public bidCounter
communityCounter
Last assigned community id. Ids start at 1 and are never reused.
uint256 public communityCounter
fileCounter
Last assigned media file id. Ids start at 1 and are never reused. One global sequence:
every registerMedia entry mints a fresh id — there is no on-chain notion of "the same file".
uint256 public fileCounter
profiles
Core record per uid.
mapping(uint256 uid => Profile) public profiles
recovery
Recovery (guardian) address per uid. Not subject to the one-address-one-profile rule.
mapping(uint256 uid => address) public recovery
payoutAddress
Raw tip recipient per uid; address(0) means "custody" (see payout).
mapping(uint256 uid => address) public payoutAddress
pendingRecovery
In-flight recovery per uid.
mapping(uint256 uid => PendingRecovery) public pendingRecovery
pendingCustody
Address that may acceptCustody of the uid (two-step transfer). address(0) = none.
mapping(uint256 uid => address) public pendingCustody
pendingCommunityOwner
uid that may acceptCommunity (two-step community transfer). 0 = none.
mapping(uint256 communityId => uint256) public pendingCommunityOwner
linkedUid
Profile an address belongs to (custody or linked). 0 = none. Enforces one-address-one-profile.
mapping(address => uint256) public linkedUid
permissions
Permission bitmap of addr on uid. Custody holds type(uint256).max; linked keys never do.
mapping(uint256 uid => mapping(address => uint256)) public permissions
pendingLink
Pending link offers, keyed by uid then proposed address.
mapping(uint256 uid => mapping(address addr => PendingLink)) public pendingLink
nonces
EIP-712 nonce per address, shared by Execute and LinkConsent.
mapping(address => uint256) public nonces
inviteUsed
Redeemed invite-code leaves (keccak256(code)) — genesis gate replay guard. True once spent.
mapping(bytes32 leaf => bool) public inviteUsed
nameOf
Buzz name (handle) of a uid, right-padded bytes32. 0 = none.
mapping(uint256 uid => bytes32) public nameOf
uidOfName
Reverse index of nameOf.
mapping(bytes32 name => uint256) public uidOfName
communityNameOf
Community handle. Separate namespace from buzz names.
mapping(uint256 communityId => bytes32) public communityNameOf
communityIdOfName
Reverse index of communityNameOf.
mapping(bytes32 name => uint256) public communityIdOfName
relationships
Directed relationship bitmap from uid to target (bit meanings are a client convention).
mapping(uint256 uid => mapping(uint256 target => bytes32)) public relationships
profileData
Free-form profile fields (key → value). Keys are a client convention ("name", "bio", …).
mapping(uint256 uid => mapping(bytes32 key => bytes)) public profileData
communities
Core record per community.
mapping(uint256 communityId => Community) public communities
communityKind
Self-describing kind token, set once at creation (client convention: "company", "channel", …).
mapping(uint256 communityId => bytes32) public communityKind
standing
The community's view of a member: an open 256-bit standing word (STANDING_* for the low bits).
Written by the owner or STANDING_ADMIN holders. The owner itself is never stored here.
mapping(uint256 communityId => mapping(uint256 uid => uint256)) public standing
communityData
Free-form community fields.
mapping(uint256 communityId => mapping(bytes32 key => bytes)) public communityData
memberEdge
The member's own view of a community: an open 256-bit word (follow/mute/label; client convention).
Written by the member. Independent of standing.
mapping(uint256 communityId => mapping(uint256 uid => uint256)) public memberEdge
Functions
constructor
constructor(address treasury, address inviteSigner) ;
Parameters
| Name | Type | Description |
|---|---|---|
treasury |
address |
Recipient of fees and tip shares (deploy BuzzerTreasury first). Cannot be changed. |
inviteSigner |
address |
Genesis invite gate signer. address(0) disables the gate (open from block zero); otherwise the first INVITE_CUTOFF profiles (or until INVITE_PERIOD elapses) require a voucher signed by this key. Immutable, and the gate can only expire, never re-close. |
_msgSender
The acting address: msg.sender normally, or the verified signer inside executeSigned
(whose self-call makes msg.sender == address(this)). Never returns address(0).
function _msgSender() internal view returns (address s);
_auth
Requires the actor to hold bit on uid. One SLOAD: custody holds PERM_ALL.
function _auth(uint256 uid, uint256 bit) internal view returns (address s);
Returns
| Name | Type | Description |
|---|---|---|
s |
address |
The acting address. |
_onlyCustody
Requires the actor to be custody of uid. A deleted uid (custody 0) can never match.
function _onlyCustody(uint256 uid) internal view returns (address s);
Returns
| Name | Type | Description |
|---|---|---|
s |
address |
The acting address. |
_takeFee
Exact-match fee, pushed to TREASURY at once. A failed push leaves the ETH here (see sweep)
instead of reverting, so a misbehaving treasury can never brick paid operations.
function _takeFee(uint256 amount) internal;
multicall
Execute several calls to this contract in one transaction.
Each entry is delegatecalled into this contract, so the actor (msg.sender, or the
signer inside executeSigned) is preserved and every entry is authorised on its own.
Any revert aborts the whole batch and is bubbled as CallFailed(reason).
Non-payable by design: paid operations (createProfile/createProfileInvited, setName, setCommunityName, createCommunity) must be
sent on their own so fee accounting stays exact-match. Everything else batches freely,
including several users' executeSigned calls submitted by one relayer.
function multicall(bytes[] calldata calls) external returns (bytes[] memory results);
Parameters
| Name | Type | Description |
|---|---|---|
calls |
bytes[] |
ABI-encoded calls to this contract. |
Returns
| Name | Type | Description |
|---|---|---|
results |
bytes[] |
Raw return data of each call, in order. |
sweep
Forward any balance this contract holds to TREASURY. Permissionless.
The contract never legitimately holds funds (fees and tip shares are pushed immediately), so the entire balance is always safe to forward.
function sweep(address token) external;
Parameters
| Name | Type | Description |
|---|---|---|
token |
address |
ERC-20 to sweep, or address(0) for ETH. |
useNonce
Consume the caller's current nonce, invalidating any Execute or LinkConsent message
signed with it but not yet submitted.
Uses the meta-tx actor when called through executeSigned.
function useNonce() external;
eip712Domain
ERC-5267 domain description for wallets/SDKs.
function eip712Domain()
external
view
returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
);
createProfile
Create a profile. The caller becomes custody with every permission.
Pays PROFILE_FEE plus nameFee(name) if a name is given (exact msg.value).
Reverts AlreadyLinked if the caller already belongs to a profile.
function createProfile(
address recovery_,
bytes32 name,
uint256 referrerUid,
bytes32[] calldata keys,
bytes[] calldata values
) external payable returns (uint256 uid);
Parameters
| Name | Type | Description |
|---|---|---|
recovery_ |
address |
Optional guardian address (address(0) = none). |
name |
bytes32 |
Optional buzz name (0 = none). Must be free and valid. |
referrerUid |
uint256 |
Optional referring uid (0 = none). Emitted in ProfileCreated, never stored or validated. |
keys |
bytes32[] |
Initial profile data keys (may be empty). |
values |
bytes[] |
Initial profile data values, same length as keys. |
Returns
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
The new profile id. |
createProfileInvited
Create a profile during the genesis phase, redeeming a bearer invite code.
Identical to createProfile except it clears the genesis gate. The invite proves the code is a
leaf of a Merkle root that INVITE_SIGNER (the 2-of-6 threshold signer) signed once; the code
is single-use. The redeeming wallet becomes custody — codes are not bound to an address, so they
can be distributed ahead of time (docs/invite-gate.md). Once the gate has opened (count or time)
the invite is ignored and this behaves exactly like createProfile, so it is always safe to call.
function createProfileInvited(
address recovery_,
bytes32 name,
uint256 referrerUid,
bytes32[] calldata keys,
bytes[] calldata values,
InviteProof calldata invite
) external payable returns (uint256 uid);
Parameters
| Name | Type | Description |
|---|---|---|
recovery_ |
address |
|
name |
bytes32 |
|
referrerUid |
uint256 |
|
keys |
bytes32[] |
|
values |
bytes[] |
|
invite |
InviteProof |
The invite redemption: {root, deadline, sig} (the founders' public authorization) plus the redeemer's code and its Merkle proof into root. |
_createProfile
Shared profile-creation body. The caller becomes custody with PERM_ALL.
function _createProfile(
address recovery_,
bytes32 name,
uint256 referrerUid,
bytes32[] calldata keys,
bytes[] calldata values
) internal returns (uint256 uid);
_inviteRequired
True while the genesis invite gate is closed: a signer is set, and neither the user-count cap nor the time window has been crossed. Crossing either opens it forever.
function _inviteRequired() internal view returns (bool);
_consumeInvite
Verify and burn a genesis invite redemption:
- the pool authorization is unexpired;
INVITE_SIGNER(the 2-of-6 threshold signer, via_isValidSig) signed this exactroot/deadline;- the code's leaf is in that signed
root(Merkle proof); - the code has not been redeemed before (single-use per leaf). A failure at any step reverts the whole signup, so nothing is burned on a bad redemption.
function _consumeInvite(InviteProof calldata invite) internal returns (bytes32 leaf);
_verifyProof
Sorted-pair Merkle proof verification (OpenZeppelin-compatible): pairs are hashed low‖high so the
proof is order-independent. Leaves are keccak256(bytes.concat(keccak256(code))) (double-hashed).
function _verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool);
transferCustody
Step 1 of a custody hand-off: propose newCustody. Nothing changes until it accepts.
function transferCustody(uint256 uid, address newCustody) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile to transfer. Caller must be custody. |
newCustody |
address |
Address that may call acceptCustody; address(0) cancels a pending proposal. |
acceptCustody
Step 2 of a custody hand-off: the proposed address claims the profile.
Clears the recovery address, any pending recovery and the payout address. Linked keys
are kept — the new owner should review them (removeLinks). Reverts AlreadyLinked
if the caller already belongs to a profile.
function acceptCustody(uint256 uid) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile being claimed. |
setRecovery
Set (or clear with address(0)) the recovery address. Cancels any pending recovery.
function setRecovery(uint256 uid, address r) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
r |
address |
New recovery address. May be shared across profiles. |
_cancelPendingRecovery
Clears a pending recovery if one exists, emitting RecoveryCancelled.
function _cancelPendingRecovery(uint256 uid) internal;
setPayout
Set the address that receives tips (address(0) = custody). Cleared on custody change.
function setPayout(uint256 uid, address p) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
p |
address |
Payout address. |
payout
Resolved tip recipient of uid: payoutAddress if set, else custody (0 if deleted).
function payout(uint256 uid) public view returns (address p);
addLink
Link addr to uid in one transaction, using an EIP-712 LinkConsent signed by addr.
The consent binds (uid, addr, custody, perms, nonce, deadline) where custody is the
submitting custody address, so a consent given to one owner cannot be redeemed by the next.
Consumes nonces[addr]. addr may be an ERC-1271 contract wallet.
function addLink(uint256 uid, address addr, uint256 perms, uint256 deadline, bytes calldata consentSig) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
addr |
address |
Address to link. Must not belong to any profile. |
perms |
uint256 |
Permission bitmap (a request for type(uint256).max is clamped to max >> 1). |
deadline |
uint256 |
Consent expiry timestamp. |
consentSig |
bytes |
Signature by addr over the LinkConsent struct. |
proposeLink
Two-step alternative to addLink: custody proposes, addr accepts from its own wallet.
A proposal is valid for LINK_PROPOSAL_TTL and only while the proposer is still custody.
function proposeLink(uint256 uid, address addr, uint256 perms) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
addr |
address |
Address to link. |
perms |
uint256 |
Permission bitmap; 0 revokes an existing proposal. |
acceptLink
Accept a link proposal made to the caller for uid.
Reverts Unauthorized if there is no live proposal from the current custody,
Expired if the proposal TTL passed, AlreadyLinked if the caller belongs to a profile.
function acceptLink(uint256 uid) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile to join. |
setPermissions
Change the permission bitmap of a linked key.
function setPermissions(uint256 uid, address addr, uint256 perms) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
addr |
address |
A linked key of uid (not custody). |
perms |
uint256 |
New bitmap (type(uint256).max is clamped to max >> 1). |
removeLink
Remove one linked key. Caller must be custody.
function removeLink(uint256 uid, address addr) external;
removeLinks
Remove several linked keys. Caller must be custody. Reverts on the first non-linked entry.
function removeLinks(uint256 uid, address[] calldata addrs) external;
_removeLink
Custody-side unlink; refuses custody itself.
function _removeLink(uint256 uid, address addr) internal;
unlinkSelf
A linked (non-custody) key detaches itself from whatever profile it belongs to.
Custody cannot unlink itself; use transferCustody or deleteProfile.
function unlinkSelf() external;
startRecovery
Begin recovery: after RECOVERY_DELAY, newCustody may claim the profile.
Overwrites any earlier pending recovery. Custody sees RecoveryStarted and can cancel
or rotate the guardian — this is the protection against a stolen recovery key.
function startRecovery(uint256 uid, address newCustody) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be its recovery address. |
newCustody |
address |
Future custody. Must be non-zero and not belong to any profile (checked now). |
cancelRecovery
Cancel a pending recovery. Caller must be custody or the recovery address.
function cancelRecovery(uint256 uid) external;
executeRecovery
Complete a recovery once the delay has passed. Must be called by the designated new custody.
The recovery address is kept; the new custody should call setRecovery. Clears payout.
function executeRecovery(uint256 uid) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile being recovered. |
deleteProfile
Permanently retire a profile. The uid is never reused.
Requires no linked keys (HasLinks) and no owned communities (HasCommunities) so nothing can
ever act as the dead uid and no community name is frozen. Releases the buzz name, clears custody,
recovery, pending state and payout. Indexers should treat ProfileDeleted as a full wipe.
function deleteProfile(uint256 uid) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
_setCustody
Moves custody from old to newCustody; clears pending recovery/custody and payout.
function _setCustody(uint256 uid, address old, address newCustody) internal;
_link
Links addr to uid with perms (clamped so it can never equal the custody sentinel).
function _link(uint256 uid, address addr, uint256 perms) internal;
_unlink
Unlinks addr from uid. Only reachable for addresses that went through _link.
function _unlink(uint256 uid, address addr) internal;
setName
Set or change the buzz name. Pays nameFee(name) (exact msg.value); every set pays.
Subject to NAME_CHANGE_INTERVAL once a name has ever been set or cleared.
The previous name is released immediately.
function setName(uint256 uid, bytes32 name) external payable;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Profile. Caller must be custody. |
name |
bytes32 |
Right-padded [a-z0-9]{1,20}. |
clearName
Release the buzz name. Free; subject to the 30-day interval. Reverts BadName if none.
function clearName(uint256 uid) external;
nameFee
Price of a name by length: 1 char 1 ETH, 2 → 0.1, 3 → 0.01, 4 → 0.001, 5–16 → 0.0001, 17–20 → free (0). Longer names are cheaper, so premium short handles carry the cost.
Reverts BadName for an invalid name. Applies to buzz names and community names alike.
function nameFee(bytes32 name) public pure returns (uint256);
_setName
Buzz-name write path; returns the fee due.
function _setName(uint256 uid, bytes32 name, bool checkInterval) internal returns (uint256 fee);
_claimName
Shared by buzz and community names: validates, prices, releases the old name, claims the new one.
function _claimName(
mapping(uint256 => bytes32) storage nameOf_,
mapping(bytes32 => uint256) storage idOf_,
uint256 id,
bytes32 name
) internal returns (uint256 fee);
_checkNameInterval
The interval only applies once a name has been set or cleared before (t != 0).
function _checkNameInterval(uint256 t) internal view;
_validateName
Validates [a-z0-9]{1,20} right-padded with zero bytes. Returns the length.
function _validateName(bytes32 name) internal pure returns (uint256 len);
setRelationships
Set relationship words from uid to each target (batch; 0 clears).
Bit meanings are a client convention (bit 0 follow, 1 block, 2 mute, …). Targets are not
validated. Requires PERM_GRAPH.
function setRelationships(uint256 uid, uint256[] calldata targets, bytes32[] calldata values) external;
setMemberEdge
Set the member's own edge word from uid to a community (follow/mute/label). Requires PERM_GRAPH.
Anyone may set their own edge to any community id; the value is a client convention, never validated.
function setMemberEdge(uint256 uid, uint256 communityId, uint256 value) external;
setProfileData
Write profile fields (batch; an empty value clears). Requires PERM_PROFILE.
function setProfileData(uint256 uid, bytes32[] calldata keys, bytes[] calldata values) external;
_setProfileData
function _setProfileData(uint256 uid, bytes32[] calldata keys, bytes[] calldata values) internal;
buzz
Publish one or more posts. Each emits Buzzed; ids are consecutive.
The only storage write is the counter. Nothing in items is validated. Requires PERM_POST.
function buzz(uint256 uid, BuzzInput[] calldata items) external returns (uint256[] memory bids);
Returns
| Name | Type | Description |
|---|---|---|
bids |
uint256[] |
Ids of the created posts, in input order (empty if items is empty). |
deleteBuzz
Signal that uid deletes post bid.
Ownership is NOT checked on-chain (there is no per-post storage). Indexers must ignore a
BuzzDeleted whose uid differs from the Buzzed uid. Requires PERM_POST.
function deleteBuzz(uint256 uid, uint256 bid) external;
_buzz
function _buzz(
uint256 uid,
bytes32 postType,
uint256 parentBid,
bytes32[] calldata content,
uint256[] calldata taggedUsers,
bytes32[] calldata settings
) internal returns (uint256 bid);
registerMedia
Register one or more media files for uid. Each mints a fresh fileId and emits Media.
Records-only: the only storage write is the counter. Nothing is validated. Requires PERM_POST.
function registerMedia(uint256 uid, MediaInput[] calldata items) external returns (uint256[] memory fileIds);
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Announcing profile. |
items |
MediaInput[] |
Media entries (bid/hash/config/locator each). |
Returns
| Name | Type | Description |
|---|---|---|
fileIds |
uint256[] |
Ids of the registered files, in input order (empty if items is empty). |
react
React to one or more posts, optionally tipping ERC-20 tokens.
Reactions are events only (latest wins; type 0 removes). For amounts[i] > 0:
the full amount is pulled from the acting address via transferFrom (approve first) straight
to payout(toUids[i]) — no protocol cut. A tip to a deleted/unknown uid reverts (tokens are
never burned). Requires PERM_REACT.
function react(
uint256 uid,
uint256[] calldata bids,
uint256[] calldata reactionTypes,
address[] calldata tokens,
uint256[] calldata amounts,
uint256[] calldata toUids
) external;
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
Acting profile. |
bids |
uint256[] |
Posts reacted to (not validated). |
reactionTypes |
uint256[] |
Client convention (1 like, 2 dislike, …; 0 removes). |
tokens |
address[] |
ERC-20 per entry (ignored when amounts[i] == 0). Must have code. |
amounts |
uint256[] |
Tip amounts (0 = no tip). |
toUids |
uint256[] |
Tip recipients — the post author as seen in Buzzed. Not verified on-chain: it is the tipper's own money and the client is responsible for the value. |
_react
function _react(
address from,
uint256 uid,
uint256 bid,
uint256 reactionType,
address token,
uint256 amount,
uint256 toUid
) internal;
_transferFrom
transferFrom that reverts on failure. Tolerates no-return tokens; refuses codeless addresses.
function _transferFrom(address token, address from, address to, uint256 amount) internal;
_tryTransferFrom
transferFrom that returns success instead of reverting.
function _tryTransferFrom(address token, address from, address to, uint256 amount) internal returns (bool);
_transfer
transfer that reverts on failure (used by sweep).
function _transfer(address token, address to, uint256 amount) internal;
_call
Low-level token call: success = call ok and (no return data or a true bool).
function _call(address token, bytes memory data) internal returns (bool);
createCommunity
Create a community owned by uid. Requires PERM_COMMUNITY. Pays COMMUNITY_FEE (exact msg.value).
function createCommunity(uint256 uid, bytes32 kind, bytes32[] calldata keys, bytes[] calldata values)
external
payable
returns (uint256 communityId);
Parameters
| Name | Type | Description |
|---|---|---|
uid |
uint256 |
|
kind |
bytes32 |
Self-describing type token, set once ("company", "channel", "group", …; 0 = unset). |
keys |
bytes32[] |
Initial community data keys (may be empty). |
values |
bytes[] |
Initial community data values. |
Returns
| Name | Type | Description |
|---|---|---|
communityId |
uint256 |
The new community id. |
transferCommunity
Step 1 of a community hand-off: propose newOwnerUid. Nothing changes until it accepts.
function transferCommunity(uint256 communityId, uint256 uid, uint256 newOwnerUid) external;
Parameters
| Name | Type | Description |
|---|---|---|
communityId |
uint256 |
Community. uid must be its owner (caller must hold PERM_COMMUNITY on uid). |
uid |
uint256 |
|
newOwnerUid |
uint256 |
Future owner; 0 cancels a pending proposal. |
acceptCommunity
Step 2 of a community hand-off: the proposed owner claims the community.
function acceptCommunity(uint256 communityId, uint256 uid) external;
Parameters
| Name | Type | Description |
|---|---|---|
communityId |
uint256 |
Community being claimed. |
uid |
uint256 |
Must equal pendingCommunityOwner[communityId]; caller holds PERM_COMMUNITY on it. |
setCommunityName
Set or change the community handle. Owner only. Pays nameFee(name) (exact msg.value).
Same charset, pricing and 30-day interval as buzz names; separate namespace.
function setCommunityName(uint256 communityId, uint256 uid, bytes32 name) external payable;
clearCommunityName
Release the community handle. Owner only; free; subject to the interval. Reverts BadName if none.
function clearCommunityName(uint256 communityId, uint256 uid) external;
setStanding
Set a member's standing word (the community's view of the member).
The owner may set any word for anyone (except itself). A non-owner must hold STANDING_ADMIN,
may not grant STANDING_ADMIN, and may not touch a target that is itself an admin. All other
bits are free-form and uninterpreted by the contract. word == 0 clears the standing.
function setStanding(uint256 communityId, uint256 uid, uint256 targetUid, uint256 word) external;
setStandings
Batch form of setStanding with the same rules applied per entry.
function setStandings(uint256 communityId, uint256 uid, uint256[] calldata targetUids, uint256[] calldata words)
external;
leaveCommunity
A member sheds its own standing. Banned uids cannot clear their ban this way.
function leaveCommunity(uint256 communityId, uint256 uid) external;
setCommunityData
Write community fields (batch). Owner or an admin (STANDING_ADMIN).
function setCommunityData(uint256 communityId, uint256 uid, bytes32[] calldata keys, bytes[] calldata values)
external;
_setCommunityData
function _setCommunityData(uint256 communityId, bytes32[] calldata keys, bytes[] calldata values) internal;
communityBuzz
Publish one or more posts into a community. Emits Buzzed and CommunityPosted per post.
RECORDS-ONLY: the only checks are that the community exists and the caller holds PERM_POST.
Standing (member/banned/can-post/…) is NOT enforced here — it is an advisory signal clients
and indexers filter on, exactly as they filter replies and reactions.
function communityBuzz(uint256 communityId, uint256 uid, BuzzInput[] calldata items)
external
returns (uint256[] memory bids);
Returns
| Name | Type | Description |
|---|---|---|
bids |
uint256[] |
Ids of the created posts, in input order (empty if items is empty). |
sendMessage
Send one message. The optional on-chain messaging tier — most traffic goes off-chain; this is
the opt-in path for permanence / censorship-resistance / provability. Non-payable, so it
composes in multicall and rides executeSigned gaslessly.
RECORDS-ONLY: the only check is that the caller holds PERM_MESSAGE on from, which is what
makes the sender unforgeable — the whole point of putting a message on-chain. to is a uid or a
communityId (a settings bit says which) and is never validated; unwanted messages are filtered
client-side by the relationship graph. content is opaque (typically an encrypted envelope).
function sendMessage(uint256 from, uint256 to, bytes32 settings, bytes calldata content) external;
Parameters
| Name | Type | Description |
|---|---|---|
from |
uint256 |
Authenticated sender uid. Caller must hold PERM_MESSAGE on it. |
to |
uint256 |
Recipient uid or communityId (unvalidated). |
settings |
bytes32 |
Self-describing flags word (docs/encrypted-messaging.md §6.2). |
content |
bytes |
Opaque payload (usually an encrypted envelope; may be inline ciphertext or a CID). |
executeSigned
Execute data (an ABI-encoded call to this contract) on behalf of signer.
Anyone may relay. The signer authorises Execute(signer, keccak256(data), nonce, deadline)
under DOMAIN_SEPARATOR; ERC-1271 is tried first for contract signers, then plain ECDSA.
The signer becomes the actor for the inner call via transient storage. Fees in msg.value
are paid by the relayer. Nesting, and re-entry from TREASURY/token callbacks while a
meta-tx is live, revert Reentrant. A revert inside data is bubbled as CallFailed.
function executeSigned(address signer, bytes calldata data, uint256 deadline, bytes calldata sig)
external
payable
returns (bytes memory ret);
Parameters
| Name | Type | Description |
|---|---|---|
signer |
address |
Address whose nonce is consumed and who becomes the actor. |
data |
bytes |
ABI-encoded call (may be multicall). |
deadline |
uint256 |
Signature expiry timestamp. |
sig |
bytes |
65-byte ECDSA signature or ERC-1271 signature bytes. |
Returns
| Name | Type | Description |
|---|---|---|
ret |
bytes |
Raw return data of the inner call. |
_hashTypedData
EIP-712 digest: keccak256("\x19\x01" ‖ DOMAIN_SEPARATOR ‖ structHash).
function _hashTypedData(bytes32 structHash) internal view returns (bytes32);
_isValidSig
Contract signers (incl. EIP-7702 delegated EOAs) are asked via ERC-1271 first; if that fails,
a 65-byte ECDSA signature from the address's own key is still accepted. v may be 0/1 or 27/28.
function _isValidSig(address signer, bytes32 digest, bytes calldata sig) internal view returns (bool);
Events
ProfileCreated
A profile was created. referrerUid is the (unvalidated) referrer, 0 if none — this event is its only record.
event ProfileCreated(uint256 indexed uid, address indexed custody, address recovery, uint256 indexed referrerUid);
InviteRedeemed
A genesis invite code was redeemed. leaf = keccak256(keccak256(code)) (hash your code to
look it up); join to ProfileCreated via uid. Emitted only while the gate is active.
event InviteRedeemed(bytes32 indexed leaf, uint256 indexed uid, address indexed redeemer);
CustodyTransferProposed
Custody transfer proposed (to == address(0) cancels).
event CustodyTransferProposed(uint256 indexed uid, address indexed to);
CustodyTransferred
Custody changed (accepted transfer or executed recovery).
event CustodyTransferred(uint256 indexed uid, address indexed from, address indexed to);
RecoverySet
Recovery address set (address(0) clears).
event RecoverySet(uint256 indexed uid, address recovery);
PayoutSet
Raw payout address set (address(0) = back to custody).
event PayoutSet(uint256 indexed uid, address payout);
LinkAdded
A key was linked. perms is the stored (clamped) value.
event LinkAdded(uint256 indexed uid, address indexed addr, uint256 perms);
LinkProposed
A link was proposed (perms == 0 revokes). Emits the raw requested perms.
event LinkProposed(uint256 indexed uid, address indexed addr, uint256 perms);
PermissionsSet
Permissions of a linked key changed.
event PermissionsSet(uint256 indexed uid, address indexed addr, uint256 perms);
LinkRemoved
A linked key was removed (by custody or by itself).
event LinkRemoved(uint256 indexed uid, address indexed addr);
RecoveryStarted
Recovery started; executable by newCustody at executeAfter.
event RecoveryStarted(uint256 indexed uid, address indexed newCustody, uint64 executeAfter);
RecoveryCancelled
Pending recovery cancelled (explicitly, or implicitly by a custody/recovery change).
event RecoveryCancelled(uint256 indexed uid);
RecoveryExecuted
Recovery executed; custody is now newCustody.
event RecoveryExecuted(uint256 indexed uid, address indexed newCustody);
ProfileDeleted
Profile permanently retired. Indexers should treat this as a full wipe of the uid.
event ProfileDeleted(uint256 indexed uid);
NameSet
Buzz name set (name == 0 cleared).
event NameSet(uint256 indexed uid, bytes32 name);
NonceUsed
A nonce was consumed via useNonce.
event NonceUsed(address indexed addr, uint256 nonce);
RelationshipSet
Relationship word set (0 clears).
event RelationshipSet(uint256 indexed uid, uint256 indexed target, bytes32 value);
ProfileDataSet
Profile field written; read the value from profileData.
event ProfileDataSet(uint256 indexed uid, bytes32 indexed key);
Buzzed
A post. This is the only record of a buzz; nothing is stored.
event Buzzed(
uint256 indexed uid,
uint256 indexed bid,
uint256 indexed parentBid,
bytes32 postType,
bytes32[] content,
uint256[] taggedUsers,
bytes32[] settings
);
BuzzDeleted
Deletion signal. NOT ownership-checked on-chain — indexers must ignore it if uid ≠ the Buzzed uid.
event BuzzDeleted(uint256 indexed uid, uint256 indexed bid);
Reacted
A reaction, optionally with a tip. toUid/to are the tip recipient (0 when no tip).
event Reacted(
uint256 indexed uid,
uint256 indexed bid,
uint256 reactionType,
address token,
uint256 amount,
uint256 toUid,
address to
);
Media
A media file was registered. This event is the only record — nothing but the id counter is stored.
uid (who announced it), fileId (the fresh global id) and hash (content identity) are indexed
so a light client can, from logs alone: list a uid's media, resolve a fileId, and group records by
hash to find the same bytes across storage locations. bid is an optional origin post (0 = none);
config is self-describing flags; locator is the bare storage id (scheme in config). See docs/media.md.
event Media(
uint256 indexed uid, uint256 indexed fileId, bytes32 indexed hash, uint256 bid, bytes32 config, bytes locator
);
CommunityCreated
A community was created. kind is the self-describing type token, set once here.
event CommunityCreated(uint256 indexed communityId, uint256 indexed ownerUid, bytes32 kind);
CommunityTransferProposed
Community transfer proposed (to == 0 cancels).
event CommunityTransferProposed(uint256 indexed communityId, uint256 indexed to);
CommunityTransferred
event CommunityTransferred(uint256 indexed communityId, uint256 indexed from, uint256 indexed to);
CommunityNameSet
Community name set (name == 0 cleared).
event CommunityNameSet(uint256 indexed communityId, bytes32 name);
CommunityStandingSet
A member's standing word set (word == 0 clears). This event is the record of membership/roles.
event CommunityStandingSet(uint256 indexed communityId, uint256 indexed uid, uint256 word);
CommunityDataSet
Community field written; read the value from communityData.
event CommunityDataSet(uint256 indexed communityId, bytes32 indexed key);
MemberEdgeSet
A member's own edge to the community set (word == 0 clears).
event MemberEdgeSet(uint256 indexed communityId, uint256 indexed uid, uint256 word);
CommunityPosted
Emitted alongside Buzzed for every post made through communityBuzz.
event CommunityPosted(uint256 indexed communityId, uint256 indexed uid, uint256 indexed bid, bytes32[] settings);
Message
A message. content is an opaque (usually encrypted) envelope; settings is a self-describing
flags word (see docs/encrypted-messaging.md §6.2). from is the authenticated sender uid; to
is a uid or communityId (per a settings bit) and is NOT validated (records-only). All three
are indexed so a light client can build its inbox straight from logs, with no indexer.
event Message(uint256 indexed from, uint256 indexed to, bytes32 indexed settings, bytes content);
Swept
Parked funds forwarded to TREASURY (token == address(0) for ETH).
event Swept(address indexed token, uint256 amount);
Errors
Unauthorized
Caller lacks the permission (or role) the action requires.
error Unauthorized();
AlreadyLinked
The address is already custody of / linked to a profile.
error AlreadyLinked();
NotLinked
The address is not a linked key of this profile (or is custody, which cannot be edited as a link).
error NotLinked();
BadFee
msg.value does not equal the required fee.
error BadFee();
BadName
Name violates [a-z0-9]{1,20} right-padded, or there is no name to clear.
error BadName();
NameTaken
Name is already registered in this namespace.
error NameTaken();
TooSoon
The 30-day name interval has not elapsed.
error TooSoon();
NoRecovery
No pending recovery, or its delay has not elapsed.
error NoRecovery();
BadSignature
Signature does not verify for the given signer/nonce/deadline.
error BadSignature();
Expired
Deadline or proposal TTL has passed.
error Expired();
LengthMismatch
Parallel arrays have different lengths.
error LengthMismatch();
BadRole
A standing write is not permitted for the caller/target (e.g. a non-admin, or touching the owner/an admin).
error BadRole();
NoPayout
Tip recipient profile has no payout address (unknown or deleted uid).
error NoPayout();
TransferFailed
ERC-20 transfer failed (or token has no code).
error TransferFailed();
Reentrant
executeSigned nested or re-entered while a meta-transaction is in flight.
error Reentrant();
CallFailed
A relayed/batched inner call reverted; reason is its raw revert data.
error CallFailed(bytes reason);
HasLinks
Profile still has linked keys; remove them before deleting.
error HasLinks();
HasCommunities
Profile still owns communities; transfer them before deleting.
error HasCommunities();
NoProfile
Referenced profile does not exist, or uid exceeds the storable range.
error NoProfile();
NothingToClaim
sweep found nothing to forward.
error NothingToClaim();
InviteRequired
The genesis invite gate is active and this signup carried no valid voucher (createProfile
while gated — use createProfileInvited).
error InviteRequired();
InviteUsed
The invite voucher's nonce was already redeemed.
error InviteUsed();
Structs
Profile
Per-profile core record. Packed into one storage slot.
struct Profile {
address custody;
uint40 nameChangedAt;
uint24 linkCount;
uint24 communityCount;
}
Properties
| Name | Type | Description |
|---|---|---|
custody |
address |
Owner address. address(0) means the uid was deleted or never existed. |
nameChangedAt |
uint40 |
Timestamp of the last setName/clearName (0 = never); gates the 30-day interval. |
linkCount |
uint24 |
Number of linked (non-custody) keys. Must be 0 to deleteProfile. |
communityCount |
uint24 |
Number of communities owned. Must be 0 to deleteProfile. |
PendingRecovery
In-flight recovery. executeAfter == 0 means none.
struct PendingRecovery {
address newCustody;
uint64 executeAfter;
}
PendingLink
Two-step link offer created by proposeLink.
struct PendingLink {
address proposer;
uint64 expires;
uint256 perms;
}
Properties
| Name | Type | Description |
|---|---|---|
proposer |
address |
Custody at proposal time; must still be custody when accepted. |
expires |
uint64 |
Proposal is void after this timestamp (LINK_PROPOSAL_TTL). |
perms |
uint256 |
Permission bitmap the key will receive. |
Community
Per-community core record. Packed into one storage slot.
kind and membership live in their own mappings (communityKind, standing, memberEdge).
struct Community {
uint160 owner;
uint64 nameChangedAt;
}
Properties
| Name | Type | Description |
|---|---|---|
owner |
uint160 |
Owner uid. 0 means the community does not exist. |
nameChangedAt |
uint64 |
Timestamp of the last community-name change (0 = never). |
BuzzInput
Input for one post in buzz / communityBuzz.
struct BuzzInput {
bytes32 postType;
uint256 parentBid;
bytes32[] content;
uint256[] taggedUsers;
bytes32[] settings;
}
Properties
| Name | Type | Description |
|---|---|---|
postType |
bytes32 |
32-byte type token, uninterpreted. Convention (docs/registry.md §4): empty (0x0) = a plaintext post (parentBid 0 = original, else reply); otherwise a right-padded ASCII tag, e.g. bytes32("poll"). |
parentBid |
uint256 |
Referenced post for replies/reposts/quotes/threads (0 for an original). Not validated. |
content |
bytes32[] |
Opaque payload (packed UTF-8, CID, …). Encoding is signalled in settings. |
taggedUsers |
uint256[] |
uids mentioned in the post. |
settings |
bytes32[] |
Client convention key/value words (encoding, media type, language, …). |
MediaInput
Input for one media file registration in registerMedia. Records-only; nothing is stored
except the id counter — the record lives in the Media event and clients index it.
struct MediaInput {
uint256 bid;
bytes32 hash;
bytes32 config;
bytes locator;
}
Properties
| Name | Type | Description |
|---|---|---|
bid |
uint256 |
Optional origin post this file was made for (0 = standalone). The durable post→file link is the post's own content; this is a convenience back-reference. |
hash |
bytes32 |
Content identity: sha256(exact file bytes). Clients verify fetched bytes against it, and group records sharing a hash to discover the same file across storage locations (e.g. an R2 upload later mirrored to Arweave). See docs/media.md. |
config |
bytes32 |
Self-describing flags word: storage scheme, hash scheme, media type, gallery index, … (client convention, uninterpreted here). |
locator |
bytes |
Bare storage id/path (Arweave txid, IPFS CID, R2 path, URL). Scheme lives in config. |
InviteProof
A genesis invite redemption. root/deadline/sig are the founders' public authorization
(signed once by INVITE_SIGNER over InviteRoot); code is the redeemer's bearer code and
proof is its Merkle path into root. Ignored once the gate has opened. See docs/invite-gate.md.
struct InviteProof {
bytes32 root;
uint256 deadline;
bytes sig;
bytes code;
bytes32[] proof;
}