Contracts

Buzzer

Git Source

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

  1. Gas first. Posts and reactions are events only (no per-post storage). Every hot path performs exactly one authorisation SLOAD.
  2. Validate only what protects funds or control. Anything that merely affects UX (reply targets, reaction types, content encoding, reserved ranges) is a client convention.
  3. 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.
  4. 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.
  5. Money never gets stuck. Fees and tip shares are pushed to TREASURY immediately; if a push fails the funds park here and anyone can sweep them 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 with multicall. 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

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)")

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

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:

  1. the pool authorization is unexpired;
  2. INVITE_SIGNER (the 2-of-6 threshold signer, via _isValidSig) signed this exact root/deadline;
  3. the code's leaf is in that signed root (Merkle proof);
  4. 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);

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.

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.

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).

Remove one linked key. Caller must be custody.

function removeLink(uint256 uid, address addr) external;

Remove several linked keys. Caller must be custody. Reverts on the first non-linked entry.

function removeLinks(uint256 uid, address[] calldata addrs) external;

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;

Links addr to uid with perms (clamped so it can never equal the custody sentinel).

function _link(uint256 uid, address addr, uint256 perms) internal;

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);

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;
}

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;
}