Smart Contract
The Logbook protocol is implemented as a Move package, a smart contract written in the Move programming language, on the Sui blockchain. The package has two modules: logbook (campaigns, responses, documents, reports) and groups (groups, memberships, member cards, lists, and campaigns for the members of groups).
#What is a Smart Contract?
A smart contract is a program that runs on the blockchain. Unlike traditional software that runs on a company's servers, smart contracts:
- Execute automatically: Once deployed, the code runs exactly as written — the rules change only through a public upgrade (see Versions and Upgrades below)
- Are transparent: Anyone can read the code and verify what it does
- Are trustless: You don't need to trust the developer — you can verify the logic yourself
- Are permanent: Once deployed, the contract exists as long as the blockchain exists
#Why Sui is Different
Most blockchains (Ethereum, Solana, etc.) have significant limitations:
#Traditional Blockchains
- Limited data storage: Storing data is extremely expensive, so most apps store minimal on-chain data
- Simple logic only: Complex programs are costly and slow to execute
- Account-based model: Data is tied to accounts, making it hard to work with individual objects
#Sui's Advantages
Sui was designed from the ground up to solve these problems:
1. Object-Centric Model Every piece of data is an "object" with its own ID. In Logbook:
- Each campaign is an object
- Each response is its own object attached to the campaign
- You can reference any specific piece of data by its ID
- Full question text
- All answer options
- Every individual response
- Vote counts (for campaigns with public answers)
3. Move Language Sui uses Move, a language designed specifically for blockchain with:
- Built-in safety guarantees (no reentrancy attacks, no overflow bugs)
- Resource-oriented programming (assets can't be accidentally destroyed or duplicated)
- Clear ownership model (every object is owned by one address or shared, like a campaign, and only the contract's code decides who may change it)
- Campaign creation and question setup happen in one transaction
- Response submission and vote counting happen together (when answers are public)
- Either everything succeeds, or nothing changes
#What This Means for Logbook
Because of Sui's unique features, Logbook can:
- Store every response permanently on-chain (not just a hash)
- Execute voting logic in the smart contract itself
- Let anyone verify public results by reading the blockchain directly, and let everyone allowed to decrypt verify private ones
- Offer a web2-like experience (Google login, sponsored transactions) with web3 guarantees
#Contract Address
On Sui Testnet:
- Package:
0x84c9ba748b84c28393cf3cb03cff01fb4697e3bd30d0a6d2cc7862f041532c63, the address of all types and the Seal namespace; it never changes with upgrades - Latest version:
0x84c9ba748b84c28393cf3cb03cff01fb4697e3bd30d0a6d2cc7862f041532c63, where functions are called (the same id until the first upgrade) - Config:
0xaa045303892c9e624ed6078da020f70684e9f1910e832b22bc27104d7738f535, shared; holds the version every function checks - Registry:
0xf7d60573a0b4d111c1080bfb3cabdbdff95ef63a25c6dc6fcc797d1dd0c288ff, shared; the parent every campaign id and every group id is derived from
GET /api/v1. The package's code is public on-chain: a Sui explorer such as Suiscan shows its two modules.
This package was published on 7 October 2026 as a new package, with campaigns and groups together. It is version 1 and has not been upgraded, so "Package" and "Latest version" are the same id. The packages before it are retired: the site, the SDK and the MCP servers do not read them, and their campaigns and groups are no longer shown. Their objects stay on-chain.
#Core Objects
#Campaign
The main shared object representing a survey or poll:
- id: Derived from the Registry, the creator's address and a random nonce (
campaign_id_for(registry, creator, nonce)), so a client knows it before the campaign exists. A creator and nonce pair can be used once, ever: even after a deletion the id is never reused - creator, nonce: Who created the campaign, and the nonce its id was derived from
- sponsor: Who paid the gas of the creation, if anyone. Every later change by the creator must be paid the same way (see Functions), so storage is always refunded to whoever paid for it
- title, description: Plaintext for open campaigns, encrypted otherwise
- questions: Vector of Question objects (text and options are encrypted for non-open campaigns)
- access: Who can open the campaign — 0 (anyone), 1 (password), 2 (address list), 3 (members of groups)
- visibility: Who can read answers — 0 (everyone), 1 (everyone with access), 2 (voters), 3 (creator), 4 (after the end date)
- allowlist, allowlist_size: Table of addresses for access = 2, and its size
- access_epoch: Grows whenever an address leaves the list; answers shared by an audience are encrypted for the current epoch
- content_version: Grows with every edit; a response names the version its answers refer to
- responses: Table of Response objects keyed by respondent address; total_responses counts them
- created_at, updated_at, end_time: Timestamps (ms)
- content_key: The campaign content key, encrypted with Seal (released to everyone with access; the creator recovers it this way)
- link_key: The content key wrapped with the campaign password (password campaigns only)
- link_pubkey: The ed25519 public key whose signatures prove the password (password campaigns only)
- audience: Campaigns for the members of groups only: the groups whose members may respond, which of them the creator named, the moment the participants were frozen (the creation time), the waiting period for new members, and the rule of who counts once that held at that moment. Empty for every other campaign
#Question
Stored within a Campaign:
- question_type: 0 (single), 1 (multiple), 2 (text)
- text: The question text
- required: Whether the question is required
- options: Vector of answer options
- allow_other: Whether an "Other" answer is allowed
- option_votes, other_votes, text_answers: Counts kept by the contract when answers are public (zero otherwise)
#Response
A participant's submission, stored as a child object of the campaign so the campaign object keeps its size:
- respondent: Address of the participant
- timestamp: Submission timestamp
- answers: Plaintext answers (question index → answer), only when answers are visible to everyone
- sealed: Encrypted answers for every other visibility (a Seal encrypted object, or content-key ciphertext for password campaigns whose answers everyone with access may read)
- key_wrap: Password campaigns only — the content key sealed for this respondent, so they can reopen the campaign on another device
- access_epoch: The access epoch the answers were encrypted for
#Registry, Config and AdminCap
- Registry (shared): The parent every campaign id and every group id is derived from; it counts the campaigns created
- Config (shared): The package version every function accepts. It also holds two settings of the module
groups: the addresses of the Logbook treasury that the payer rule protects, and the public key of the check server, which is empty until the publisher sets it: until then the contract accepts no check made by a server - AdminCap: Held by the publisher;
migrateneeds it to move the Config to a new package version, and the two settings above are set with it
#Seal Access Policy
Every Seal identity (without the package prefix) starts with the campaign id, followed by a tag and a suffix:
| Tag | Protects | Suffix |
|---|---|---|
| 0 | The campaign content key | none |
| 1 | Answers shared by an audience, and the reports of an address-list campaign whose answers everyone with access reads | the access epoch (u64, little endian) |
| 2 | One respondent's answers, and their copy of the content key | the respondent's address |
| 3 | Keys of reports about the results | none |
seal_approve function for the requesting address. Because the campaign id comes first, only the campaign a key belongs to can grant it: no other campaign, not even one with an earlier end date, can open answers sealed until the end early.
"Access" below means: the creator; anyone, on an open campaign; an address on the list; or, on a password campaign, whoever presents a proof of the password for their own address.
- Content key (tag 0): released to everyone with access (open campaigns have none)
- Shared answers (tag 1), for the current or an earlier access epoch: on "voters only" campaigns to the creator and to anyone who has responded; on address-list campaigns whose answers everyone with access may read, to the creator and the addresses currently on the list, and to a respondent removed from the list for the epochs up to that of its own response (its own answer, and answers that were all sent before its removal). Password campaigns with that setting use the content key instead of Seal. The reports about the results of such an address-list campaign (AI analysis, results certificate) are sealed the same way, for the access epoch they were made in
- One respondent's answers (tag 2): always released to that respondent; on "only creator" campaigns also to the creator; on "after the end date" campaigns to everyone with access once
end_timehas passed, and before that to nobody else, the creator included - Report keys (tag 3): released to whoever may read the results: voters and the creator on "voters only" campaigns, the creator on "only creator" campaigns, the creator and (after the end) everyone with access on "after the end date" campaigns, and everyone with access otherwise. The exception is an address-list campaign whose answers everyone with access reads: its reports are sealed under tag 1 for the current access epoch (see above), because the tag 3 identity has no epoch and a key fetched once would open every later report. The contract releases tag 3 of such a campaign to the creator only
logbook:link-access:v1 || campaign id || the sender's address. A proof works for that one address, so passing it on lets nobody else in.
Removing an address from the list starts a new access epoch. Shared answers submitted afterwards, and reports made afterwards on a campaign whose answers everyone with access reads, are encrypted for the new epoch, and an address that is no longer on the list gets no keys as a list member. What it decrypted before it keeps, and the content key (tag 0) does not change: with the copy it fetched while listed it can still read the title, questions and documents, also ones changed after its removal. A removed address that has already responded to a "voters only" campaign still reads the answers as a voter.
Campaigns for the members of groups use the same four tags, checked by seal_approve_group_campaign of the module groups. "Access" there means: the creator, or an address whose membership in one of the campaign's groups covers the moment the campaign was created (see The groups Module below). The access epoch of such a campaign stays 0, because its participants never change. A campaign of public groups only has no content key: its title and questions are plain text. seal_approve releases nothing for these campaigns.
The contract never sees plaintext: encryption and decryption happen in the browser.
#Functions
The functions of the module logbook come first; the module groups has a section of its own below. The functions below are public and can be called from a programmable transaction, except seal_approve, an entry function for the key servers; the TxContext argument is left out. Each one first checks the version in the Config and aborts with code 6 if this package version has been retired.
The creator's functions (update_campaign, delete_campaign, update_allowlist, set_documents, set_document_storage, attach_report, detach_report) must be paid the way the campaign was created: sponsored by the same sponsor if its creation was sponsored, with the creator's own gas if it was not (code 18 otherwise). Storage these calls add or free is then always paid for and refunded to the same account, so nobody can have a sponsor pay for storage and collect the refund on their own gas. A sponsor that changes its key keeps the old one for the campaigns it sponsored.
#create_campaign
Creates a new campaign on-chain, optionally with the documents it is about.
create_campaign(config, registry, nonce, title, description, question_types, question_texts, question_required, question_allow_other, all_options, options_per_question, access, visibility, allowlist, content_key, link_key, link_pubkey, end_time, doc_names, doc_mimes, doc_sizes, doc_content_hashes, doc_salts, doc_stored_hashes, doc_storages, doc_blob_ids, doc_end_epochs, clock)
Parameters:
- config: &Config, registry: &mut Registry
- nonce: address (with the sender, determines the campaign id)
- title, description: String (encrypted client-side unless access = 0)
- question_types: vector<u8>, question_texts: vector<String>, question_required: vector<bool>, question_allow_other: vector<bool>
- all_options: vector<String>, options_per_question: vector<u64> (the options of all questions, flattened)
- access: u8, visibility: u8 (an open campaign cannot use visibility 1)
- allowlist: vector<address> (access = 2 only, at least one address)
- content_key: vector<u8> (Seal encrypted, empty for open campaigns)
- link_key: vector<u8> (password-wrapped key) and link_pubkey: vector<u8> (32 bytes), password campaigns only
- end_time: u64 (ms, in the future, at most 3653 days ahead)
- doc_names … doc_end_epochs: one entry per document in each vector; empty vectors for none
- clock: &Clock
#update_campaign
update_campaign(config, campaign, title, description, question_types, question_texts, question_required, question_allow_other, all_options, options_per_question, end_time, clock)
Replaces title, description, questions and end time (creator only, only while the campaign has no responses and before its end: code 3 after it). A campaign for the members of groups cannot be edited (code 7). The new end time must be in the future and at most 3653 days ahead. Access, visibility, keys and the address list stay as they are. Increments content_version.
#delete_campaign
delete_campaign(config, campaign, clock)
Removes a campaign without responses, with its documents and reports (creator only, also after its end). A campaign for the members of groups is refused here (code 7): it is deleted with delete_group_campaign of the module groups, which also takes it off the list of every group it reaches. The address list must be empty (code 21): remove every address with update_allowlist in the same transaction first, as the Logbook clients do, so the storage of the list entries is refunded too. One transaction removes up to about 900 addresses (Sui loads every removed entry, and about 1000 objects per transaction). Like every creator function it is paid the way the campaign was created, so the storage rebate goes back to whoever paid for the storage.
#update_allowlist
update_allowlist(config, campaign, add, remove, clock)
Adds and removes addresses (add, remove: vector<address>) of an address-list campaign in one call, creator only, at any time, also after the end. Removing anyone starts a new access epoch.
#submit_response
submit_response(config, campaign, content_version, access_epoch, question_indices, answers, sealed, key_wrap, access_proof, clock)
Parameters:
- content_version: u64, access_epoch: u64 (the campaign's values the answers were prepared for)
- question_indices: vector<u64>, answers: vector<String> (plaintext, only when visibility = 0)
- sealed: vector<u8> (encrypted answers, at most 16 KiB, required for every other visibility)
- key_wrap: vector<u8> (password campaigns only: the content key sealed for the respondent)
- access_proof: vector<u8> (password campaigns: the 64-byte proof of the password; empty otherwise)
- The campaign is not one for the members of groups (code 7): those take responses through
submit_group_responseof the modulegroups - The campaign has not ended
- content_version and access_epoch are the campaign's current ones: a response prepared before an edit, or before an address was removed from the list, is refused, and the client prepares it again
- The sender has not responded yet
- The sender has access: on the list for address-list campaigns, a valid proof of the password for password campaigns
- Plaintext answers only when answers are public, encrypted answers otherwise
- Single choice: "2", or "other:<text>"
- Multiple choice: option indexes in strictly increasing order, "0,2,5", optionally followed by ",other:<text>", or "other:<text>" alone; the text runs to the end of the answer, commas included
- Text: any non-empty string
- Option indexes are plain decimals, without signs or leading zeros
#seal_approve
seal_approve(id, config, campaign, access_proof, clock)
An entry function that Seal key servers simulate before releasing the key for identity id. It aborts unless the policy above allows the sender. access_proof is the proof of the password on password campaigns, empty otherwise.
#set_documents / set_document_storage
set_documents(config, campaign, doc_names, doc_mimes, doc_sizes, doc_content_hashes, doc_salts, doc_stored_hashes, doc_storages, doc_blob_ids, doc_end_epochs, clock)
set_document_storage(config, campaign, index, storage, blob_id, walrus_end_epoch)
A campaign commits to up to 10 files (the app currently limits this to 5): name, type, size, a SHA-256 commitment to the file, the SHA-256 of the stored bytes, and where the file is stored (a Walrus blob id, or the Logbook cache). The commitment is written in the creation transaction; set_documents can replace the documents only while the campaign has no responses and has not ended, and increments content_version; it refuses a campaign for the members of groups (code 7), whose documents are fixed at creation. set_document_storage only updates where the file lives (also after the end) and can never change a hash. Open campaigns commit to the plain SHA-256 of the file; private campaigns commit to SHA-256("logbook:document:v1" || salt || file) with a random 32-byte salt, which is stored encrypted with the campaign content key, like the file name. A private document also hides its size and type: the stored file is padded (to a power of two of at least 4 KiB, up to the upload limit), so size holds the padded size and mime is empty, and the real name, type and size are encrypted together in name. Documents from before this (format v1) show their exact size and type, with only the name encrypted.
#attach_report / detach_report
attach_report(config, campaign, kind, storage, blob_id, content_hash, size, encrypted, walrus_end_epoch, clock)
detach_report(config, campaign, kind, clock)
The creator records a pointer to a document stored off-chain: its kind (0 AI analysis, 1 results certificate), where it is stored (a Walrus blob id, or the Logbook cache), the SHA-256 of the stored bytes, size and whether it is encrypted. Reports of campaigns with restricted access or visibility must be encrypted. Stored as dynamic fields of the campaign, one per kind.
#migrate
migrate(admin_cap, config)
Called in an upgraded package with a higher version: it moves the Config to that version, and from then on the functions of older package versions abort.
#The groups Module
The module groups holds everything about groups. The Groups pages of this documentation describe it for users; this section names the pieces for developers.
#Words
The site and the contract use different words for the same things. The pages say what a person reads; the contract's identifiers keep the older names.
| On the site | In the contract |
|---|---|
| list | roster (roster_root is the Merkle root of the list) |
| seat | roster entry |
| check (World ID check, email check, a seat on the list) | provider, proof, uniqueness key |
| who counts once | the providers and the roster unit of the scope root |
| community, its top group | uniqueness scope, scope root |
| how people join | admission (the ADMIT_* bits) |
| paused link | suspended link |
| waiting period for new members | settle period (settle_ms) |
| the moment the participants are frozen | cut-off (cutoff_ms) |
| participants of a campaign | audience |
#Objects
- Group (shared): creator and nonce (its id is derived from the Registry, the creator and the nonce, with another key type than a campaign's, so the two never collide), the sponsor of its creation if that was the Logbook treasury, the owner, the table of admins (at most 50, pending appointments included), name, description and card form (plain text when public, encrypted with the group key when private), the rule (how people join, whether joining is open, the required checks, whether a card is required), the root of its list and what a seat is (a person or a unit), invitations, applications, the community it belongs to, its parent and the state of that link, the groups under it and those that ask to be, the campaigns that reach it, pending appointments and a pending offer of the ownership, and two counters called epochs:
key_epochfor what members read andadmin_epochfor what admins read - MemberShard (shared, up to 16 per group, created at the first join into each): member records and member cards of the addresses whose first hexadecimal digit selects that shard. A member record is never deleted. It holds up to 64 intervals, each with the time of joining, the time of leaving if any, the way of joining and the checks proven
- KeyShard (shared, up to 16 per community, at its top group): which address holds which seat or check of the community. One key belongs to one address in the whole community
- A group is created by value:
begin_grouporbegin_group_in_scopereturns it, the same transaction sets it up, andshare_groupshares it. A transaction that begins a group shares it or fails as a whole
#Functions by Area
Every function checks the version in the Config, like the functions of logbook. Functions that change a group take a Path: the proof, built in the same transaction with path_from and path_up, that the sender is the owner or an admin of the group or of a group above it.
- Creating:
begin_group,begin_group_in_scope(a group inside a community, by an admin of its top group),share_group,open_shard,share_shard - Details and rule:
set_metadata,set_privacy,set_admission,set_admission_open,set_roster,set_list_file - Invitations:
create_invite,revoke_invite - Joining and leaving:
join(the Join button),join_with_invite,join_with_roster(a seat on the list),join_with_domain(an organisation mailbox),renew,leave,leave_open_group - Applications:
apply,withdraw_request,approve,reject,lift_rejections - Members:
remove_members,lift_removals; from the top group of a community alsoscope_remove_membersandscope_rejectin any group of it - Member cards:
card_parts,set_card,remove_card - Seats and checks:
prove_roster_entry(a seat taken with its personal link),prove_held_entry(a seat already held, shown in another group of the community),prove_attestedandprove_held(checks by the check server),begin_release,check_release,finish_release(a seat taken from its address),lift_release_bar,unlist_key - Admins and ownership:
appoint_admins,accept_admin,decline_admin,withdraw_appointments,remove_admins,take_seat,offer_ownership,accept_ownership,withdraw_offer - The tree:
propose_parent,withdraw_proposal,accept_child(followed bywalk_linkandfinish_link, which check the tree after the link is written),decline_proposal,suspend_child,set_roster_trust,detach_parent,remove_child - Campaigns:
begin_audience,add_audience_root,add_audience_group,create_group_campaign,submit_group_response,delete_group_campaign,forget_campaign,finish_campaign_removal - End of a group:
archive,delete_shard,delete_key_shard,delete_group - Publisher only, with the AdminCap:
set_protected_sponsors,set_attester - Seal policies (entry functions that key servers simulate):
seal_approve_group,seal_approve_group_admin,seal_approve_card,seal_approve_request,seal_approve_admin_file,seal_approve_group_campaign
#Campaigns for the Members of Groups
create_group_campaign takes the arguments of create_campaign, with the participants in place of access, address list and password keys. The participants are built in the same transaction: begin_audience (with the waiting period, at most 30 days), add_audience_root for each group the creator names and administers, add_audience_group for each group under one of them through an active link. At most 64 groups; an archived group is refused. Each group added writes the campaign's id into its own list of campaigns.
The campaign stores the result: the groups, the creation time, the waiting period, and whether one response per seat or check is guaranteed (unique: every group is of one community, and that community requires a check). It needs a content key when one of the groups is private.
submit_group_response takes the sender's member shard. It accepts the response when that shard's group is among the campaign's groups and one interval of the sender's record covers the creation time:
- it began strictly before the creation time, earlier by at least the waiting period
- it had not ended by then
- a check with an expiry had not run out by then
- when
unique: the membership was admitted with the checks the campaign names
submit_response: the end time, the content version, one response per address.
delete_group_campaign applies the rules of delete_campaign (the creator, paid the way the campaign was created, no responses) and returns a value that names the campaign's groups. forget_campaign takes the campaign off the list of one group, and finish_campaign_removal refuses to finish while a group is left. A campaign with responses is never deleted.
#Seal Identities of Groups
Each identity starts with the group id, followed by a tag and a suffix:
| Tag | Protects | Suffix | Released to |
|---|---|---|---|
| 4 | The group key of a private group: name, description, card form | the key epoch the texts are encrypted for | the owner, the group's own admins, current members of this group |
| 5 | The members part of a card | the card owner's address, the key epoch | the card's owner, the owner and the group's own admins, current members |
| 6 | The admins part of a card | the card owner's address, the admin epoch | the card's owner, the owner and the group's own admins |
| 7 | The text of an application | the applicant's address, the admin epoch | the applicant, the owner and the group's own admins, while the application waits |
| 8 | The admins' file (the admins' copy of a list) | an admin epoch | the owner and the group's own admins, for the current admin epoch or an earlier one |
take_seat).
key_epoch grows when a member is removed, when a member leaves (except while the Join button works, since anyone may join such a group), when an admin is removed, when the owner changes, and when the Join button stops working. admin_epoch grows when an admin is removed and when the owner changes. What is written afterwards is encrypted for an identity the former reader never had a key for. What they read before, they may have kept.
A field that must be encrypted has to start with the four bytes 4c 42 53 01 and be at least 32 bytes long (code 158 otherwise), so that a client mistake cannot publish readable personal data in such a field. The contract cannot tell real ciphertext from random bytes.
#The Payer Rule
The Config lists the addresses of the Logbook treasury. A group, an application, a card, a shard or a seat's entry that the treasury paid for remembers it. Two rules follow:
- A group is changed the way it was created. Calls of the owner and the admins that write a group created with sponsored gas must be paid by the treasury, and such calls on a group created with its creator's own gas must not be (code 105)
- What the treasury paid for, the treasury frees. A record that remembers the treasury is deleted only in a transaction the treasury pays for, so its storage refund goes back to the treasury
#The Check Server
prove_attested, check_attested and join_with_domain need a statement signed by the Logbook check server, whose public key the publisher writes to the Config with set_attester. While the key is empty they abort with ENoAttester, and no group can be joined through an email check or an organisation mailbox. The server for the email check is part of the site (see The Email Check); it signs only when it holds its keys and the Config holds its public key. The contract checks the signature and nothing else: it cannot see a mailbox. A statement says which community, which address, a keyed code of the mailbox, the day it ends, and, for a group that lets people in by its mail domain, that domain as the group stores it. The World ID check has no server yet. Seats on a list need no server.
#Versions and Upgrades
Every function of the Logbook package that changes a record or releases a key (seal_approve included) checks the version stored in the shared Config against the version of its own package; a function that only reads a value does not. The ballot package and the package of the vault’s key policy are packages of their own: their functions (cast, fix_roll, call_off and their seal_approve functions) take no Config and check no version, so neither migrate nor an upgrade of the Logbook package stops them. After an upgrade that raises the version, migrate moves the Config forward, and the functions of older package versions stop working: a fix cannot be bypassed by calling the old code.
The package deployed now is version 1 and has had no upgrade (see Contract Address). The version check, migrate and the AdminCap are there for the upgrades to come.
During the beta the package is upgradeable, and the UpgradeCap that allows upgrades is held by Logbook. An upgrade is a public transaction on Sui, but it can change any rule of the contract, including the Seal policy that decides who may decrypt what. Until that changes, the holder of the UpgradeCap is part of what you trust.
Plainly: the UpgradeCap is held by one key today, not by a multisig and not behind a delay. The Seal key servers release keys by whatever policy the current package has. So Logbook could publish an upgrade whose policy serves every key to an address of its choice, and read every private campaign, answer, member card, application and list. Everyone would see the upgrade; nothing prevents it. "Encrypted, also for Logbook" is true of Logbook's servers, which hold no key, with one exception: the session of an app connected through the remote MCP connector (see Clients and Where Keys Live). It is not yet true of Logbook as the holder of this key.
#Limits
- Up to 100 questions per campaign and 100 options per question
- One plaintext answer: at most 4096 bytes; encrypted answers: at most 16 KiB per response
- Up to 10 documents per campaign
- Groups: at most 50 admins of a group (pending appointments included), a tree of at most 8 levels, at most 64 groups in one campaign, at most 200 applications waiting in a group and 100 invitation links stored at a time
- Groups: a member card of at most 2048 bytes (both parts together, encrypted), a card form of at most 4096 bytes, the encrypted text of an application at most 1024 bytes, a name of at most 1024 bytes and a description of at most 16,000 bytes
- Groups: a rejected applicant applies to the same group again after 7 days; a waiting period for new members of at most 30 days; batches of at most 100 addresses per call (approvals, rejections, removals, appointments)
#Known Limitations
- The first response freezes a campaign. From the first response on, the questions and documents can no longer be changed and the campaign can no longer be deleted. Anyone can respond to an open campaign right after it is created (a bot watching for new campaigns needs seconds), so its creator may never get to edit it. To keep editing after publishing, create the campaign with a password: nobody without the link can respond, so it stays editable until you share the link.
- The content key does not change. Questions, documents and the title of a private campaign are encrypted with one key that is fixed at creation. An address removed from the list (or anyone who knew the password) keeps the copy it fetched, and can read the content also after you change it. Answers and reports follow the access epoch; the content does not. Rotating the key would break every share link and every respondent's copy, so it is not done; put content that a removed address must not see into a new campaign.
- Every creation goes through one object. Each new campaign and each new group writes the shared Registry its id is derived from, so all creations are ordered through that object. When the network is congested, or someone floods it with creations at a higher gas price, some creations are delayed or cancelled; a cancelled creation claimed nothing and can be sent again. Responses and all other functions never touch the Registry.
#Error Codes
A failed transaction names the abort code of the function that stopped it:
| Code | Name | Meaning |
|---|---|---|
| 0 | EAlreadyResponded | The sender has already responded |
| 1 | EInvalidAnswer | A plaintext answer breaks the answer rules, or a required question is unanswered |
| 2 | ENotAllowed | The sender has no access: not on the address list, or no valid proof of the password |
| 3 | ECampaignEnded | The campaign has ended: no more responses, edits or document changes |
| 4 | ENotCreator | Only the creator can do this |
| 5 | EInvalidQuestion | A question index does not exist or is out of order |
| 6 | EWrongVersion | This package version has been retired |
| 7 | EInvalidMode | Invalid combination of access and visibility, an address-list change on another kind of campaign, or a campaign for the members of groups given to a function of logbook that does not take one (edit, documents, response, deletion) |
| 8 | EHasResponses | The campaign has responses, so it can no longer be edited or deleted |
| 9 | EInvalidCampaign | Invalid campaign data: title, questions, options, keys or address list |
| 10 | EInvalidPayload | The response is not in the form this campaign expects |
| 11 | ENoAccess | Seal policy: this key is not released to the sender |
| 12 | EInvalidIdentity | Seal policy: the identity is malformed |
| 13 | EInvalidReport | Invalid report pointer, or an unencrypted report of a private campaign |
| 14 | ENoReport | There is no report of this kind |
| 15 | EInvalidDocument | Invalid document data |
| 16 | EContentChanged | The campaign was edited after the response was prepared |
| 17 | EStaleAccessEpoch | An address was removed from the list after the response was prepared |
| 18 | EWrongSponsor | A creator function paid differently from the creation: a sponsored campaign needs the same sponsor, one the creator paid for needs the creator's own gas |
| 19 | EInvalidEndTime | The end time must be in the future and at most 3653 days ahead |
| 20 | ENotUpgraded | Nothing to migrate: the Config already has this version |
| 21 | EAllowlistNotEmpty | A campaign is deleted only with an empty address list: remove every address first, in the same transaction |
groups has abort codes of its own, from 101 to 187. A failed transaction names the module, so 105 of groups is not confused with a code of logbook. The ones a user of the site can meet:
| Code | Name | Meaning |
|---|---|---|
| 102 | ENotOwner | Only the owner of the group can do this |
| 103 | ENotAdmin | Only the owner or an admin can do this |
| 104 | ETooManyAdmins | A group has at most 50 admins, pending appointments included |
| 105 | EWrongSponsor | A group is changed the way it was created: with sponsored gas if its creation was sponsored, with the sender's own gas if not |
| 107 | ETooDeep | A tree has at most 8 levels |
| 110 | EOpenChild | A group with a Join button does not hang under a private group |
| 112 | EAdmissionClosed | Joining is closed |
| 115 | EArchived | The group is archived |
| 116 | EAlreadyMember | The sender is a member already |
| 121 | EInviteExpired | The invitation has expired |
| 122 | EInviteUsedUp | Every place of the invitation has been taken |
| 126 | EAlreadyApplied | An application of this address is waiting |
| 127 | EPendingFull | 200 applications are waiting in this group |
| 132 | EKeyBound | The seat or check belongs to another address of the community |
| 134 | ENoAttester | The key of the check server is not set: the email check is not switched on for this deployment, and the World ID check is not built |
| 137 | ECardTooLarge | A member card holds at most 2048 bytes |
| 138 | EStaleEpoch | A card, a text or a copy was encrypted before the group's keys changed: encrypt it again |
| 140 | ETooManyGroups | A campaign names at most 64 groups |
| 144 | ENotInAudience | The group is not among the campaign's participants |
| 145 | ENotEligible | The sender's membership does not cover the moment the campaign was created |
| 147 | EHasHistory | A group that a campaign names, or that had members, is not deleted |
| 152 | ERemovedByAdmin | An admin ended this membership: only an admin reopens it |
| 153 | ENoGrant | The parent was not allowed subgroups of its own |
| 154 | EStaleRule | The rule changed since it was read |
| 168 | ECardRequired | The group takes nobody without a card |
| 169 | ENoSponsorList | A sponsored call, and the Config has no treasury addresses yet |
| 174 | ECoolingDown | The address was rejected by this group less than 7 days ago |
| 175 | EScopeRule | Who counts once is set by the top group of a community, not by a group inside it |
| 183 | EAppointmentLapsed | Whoever made the appointment is no longer the owner or an admin |
| 184 | EChangedSinceOffer | The group changed after the new owner looked at it |
| 187 | ENoSeat | An admin of a group above takes a seat among the group's admins before it appoints |
GROUP_ERROR and explains each code with explainTransactionError.
#View Functions
Read-only functions to query campaign data:
creator,nonce,sponsor,access,visibility,end_time,total_responses,content_version,access_epoch,allowlist_sizehas_responded(campaign, address),is_allowlisted(campaign, address)questions,option_votes,other_votes,text_answersdocuments,document_content_hash,document_blob_id,has_report(campaign, kind),report(campaign, kind),report_content_hashregistry_campaigns(registry),config_version(config),campaign_id_for(registry, creator, nonce)audience(campaign)and, on its value,audience_groups,audience_roots,audience_cutoff_ms,audience_settle_ms,audience_unique,audience_providers,audience_scope,audience_roster_unit- Constants:
access_open(),access_link(),access_allowlist(),access_group(),vis_everyone(),vis_access(),vis_voters(),vis_creator(),vis_after_end(),tag_content(),tag_shared(),tag_own(),tag_reports(),report_ai_analysis(),report_results_certificate(),storage_walrus(),storage_cache()
#Events
The contract emits events for tracking all operations on the blockchain. These events are used for efficient querying and indexing.
#CampaignCreated
Emitted when a new campaign is created.campaign_id: Unique ID of the campaigncreator: Address of the campaign creatoraccess: Who can open the campaignvisibility: Who can read the answersend_time: Campaign end timestamptimestamp: When the event occurred
#CampaignUpdated
Emitted when a campaign is updated (before any responses).campaign_id: Unique ID of the campaigncreator: Address of the campaign creatorcontent_version: The version after the updateend_time: The end time after the updatetimestamp: When the update occurred
#CampaignDeleted
Emitted when a campaign is deleted (only possible with zero responses).campaign_id: Unique ID of the deleted campaigncreator: Address of the campaign creatortimestamp: When the deletion occurred
#ResponseSubmitted
Emitted when a participant submits a response.campaign_id: Campaign that received the responsecreator: Campaign creator's addressrespondent: Address of the participantresponse_id: ID of the Response objectresponse_index: Index of this response (0-based)timestamp: When the response was submitted
#AllowlistChanged
Emitted when the address list changes.campaign_id: The campaignadded,removed: The addresses actually added and removedsize: Size of the list after the changeaccess_epoch: The access epoch after the changetimestamp: When the change happened
#DocumentsSet
Emitted when a campaign commits to documents or replaces them.campaign_id,creator,count(number of documents),content_version(the version the documents belong to),timestamp
#DocumentStorageSet
Emitted when the creator records where a document's file is stored now. The commitment does not change.campaign_id,creator,index,storage,blob_id,walrus_end_epoch(no timestamp: the function takes no clock; use the checkpoint's)
#ConfigMigrated
Emitted bymigrate.
from_version,to_version
#ReportAttached / ReportDetached
Emitted when the creator attaches or removes a report.campaign_id,creator,kind,timestamp- ReportAttached also carries
storage,blob_idandcontent_hash
#Events of the groups Module
Every change of a group emits an event of the module groups:
- The group:
GroupCreated,GroupUpdated(name, description, card form or visibility),AdmissionChanged(how people join),RosterSet(a new list),ListFileSet(the files of a list),GroupArchived,GroupDeleted - Admins and ownership:
AdminsAppointed(appointments made, withdrawn or declined),AdminsChanged,OwnershipOffered,OwnershipTransferred - The tree:
ParentProposed,ProposalClosed,ChildAccepted,RosterTrustSet,LinkSuspended,ParentDetached - People:
InviteCreated,InviteRevoked,JoinRequested,RequestClosed,MemberJoined(with the way of joining),MemberLeft(with who removed the member, if an admin did),RemovalsLifted,RejectionsLifted,BatchSkipped,CardSet,CardRemoved - Seats and checks:
KeyBound,KeyReleased,ReleaseBarLifted,KeyUnlisted,AttestationRenewed - Campaigns:
GroupCampaignCreated(the groups, the creation time, the waiting period, and who counts once), next toCampaignCreated;GroupCampaignDeleted, next toCampaignDeleted - Publisher:
ProtectedSponsorsSet,AttesterSet