Abstract
This ERC defines a framework through which autonomous agents — and any other account that qualifies — form a swarm: a purpose-bound collective with an explicit membership boundary, an objective with attainment rules, and a declared collaboration structure, recorded as one interoperable on-chain object. It specifies:
- a Swarm Specification: an immutable, content-addressed set of executed parameters — roles and their eligibility modules, phases with their collaboration posture and boundary, one authority per decision point, the activation rule, the contribution mode and the liveness fallbacks — identified by
specHash = keccak256(abi.encode(spec)); - a Charter: the off-chain Swarm Pack committed by
charterHash, carrying the charter text, the objective descriptor, the members' economic commitment, the collaboration structure, the roles and the resource manifest; members accept a charter version, the objective and the commitment carry hashes of their own, and an amendment that changes either is material by construction and opens an exit window; - a lifecycle with procedural states (
FORMING,ACTIVE,SUSPENDED,CONCLUDING,DISSOLVED), an outcome type kept separate from the state (ACHIEVED,PARTIAL,FAILED,ABANDONED,SUPERSEDED), settlement left to the swarm's account or to the bound obligation, and a per-swarm revision counter that orders every change; - a membership ledger in which admission is a two-sided act (an authority admits, the member accepts the charter), leaving is always available, eligibility is re-evaluable by anyone through a challenge, and a compromised or retired member is suspended, expelled or substituted under rules fixed in advance;
- phases whose collaboration posture is a coordinate over five dimensions — payoff, topology, coordination, boundary and admission — the first three drawn from an extensible on-chain vocabulary, so that cooperation, competition, co-opetition, tenders, staged elimination, debate, ensembles and pipelines are points and trajectories in one space rather than entries in a closed list;
- an
ISwarmRegistryinterface holding the normative record and executing only the transitions it names, each gated by the authority the specification assigns, and never moving value.
The minimal conforming configuration is a single-phase gated swarm whose decision points are held by designated addresses. Collective decisions through ERC-8436, member identities through ERC-8004, eligibility modules over trust-assertion and identity registries, binding to a token-bound task tender, non-agent resources with verifying adapters, signed charter acceptance, succession and nesting are normative-optional profiles over the same objects and the same state machine.
Motivation
Agents are beginning to work in groups. A proof is split into lemmas and farmed out; a market report is drafted by one agent, audited by a second and signed off by a third; a standing council answers queries; a tender attracts bids, selects a few bidders and has them build together. Each of these is a swarm: a collective that exists for a purpose, has an inside and an outside, and ends. Today the group itself has no on-chain form. The task has one, the agent has one, a single joint action has one — but the organization that connects them is improvised per platform, and no counterparty can read, in one place, who is in it, why it exists, how it works, who may change that, and whether it has ended.
Existing proposals each hold one corner. ERC-8001 standardizes a coordinated act: an initiator's intent, every participant's acceptance, one execution; it has no membership change, no objective, no phases and deliberately leaves threshold policies to modules. ERC-8063 reads a token balance as group membership and tiers; a balance can gate a swarm's roles, but a working group also needs an accepted charter, roles with scope, work commitments and closure semantics. ERC-8436 standardizes how a group decides — policies, bodies, finality — and leaves what the group is to its relying contracts; its motivation already names a swarm charter as one of them. ERC-8004 gives each agent an identity and a place for trust signals, not a group. ERC-8033 fixes one specific group shape — information agents and a judge — for one kind of query. ERC-8183 binds one job to one provider. Task-tender standards define what is demanded and how it is judged, and allow the fulfiller of record to be a team contract, without saying what a team is.
This ERC is the missing layer: the organization within which acts are coordinated, decisions are taken and tasks are fulfilled. Its independent contribution is a group instance with verifiable admission (both sides of every membership are recorded), traceable membership change (every change has an authority, a cause and a revision), explicit organizational powers (sixteen decision points, each held by a declared authority) and termination semantics (an outcome recorded apart from the state, with a permissionless exit from every state). Three design facts drive it.
The boundary is the first-class fact. Whether an account is inside a swarm determines what it may do, what it may receive and what it is accountable for. The framework therefore records membership as a ledger with explicit states and makes every change to it a named transition with a named authority: admission by an authority and acceptance by the member; departure at the member's sole discretion; removal, suspension and reinstatement by judgment; automatic suspension when an eligibility predicate — a trust assertion, an identity state, a reputation threshold — stops holding; substitution for a successor account. Agents malfunction, get compromised, change controllers or retire; the kernel cannot see that, but it can make sure that when the trust layer says so, the boundary moves.
An objective, not a calendar, ends an organization — but a clock always guards it. A swarm exists while its purpose is open. Its primary ends are therefore the recorded attainment or failure of its objective or a decision, and the loss of a viable roster pauses it; durations are not success criteria. Yet an organization that can never be closed locks escrow and breeds zombies, so every state has a permissionless exit after a declared fallback — a recruitment window, a hard cap, an inactivity timeout, a settlement window — and a specification without them is invalid. Time is the guard, never the definition.
Collaboration modes are coordinates. "Cooperation", "competition", "tender" and "elimination" are not alternatives on one list: the first two answer how outcome value is divided, the last two answer how the roster changes across phases. This ERC separates the questions — payoff, work topology, coordination, boundary, phase admission — and declares a swarm's posture as a coordinate per phase. Several solving paths may compete while every member shares the reward; a tender is a competitive open phase followed by a cooperative closed one. Named modes become profiles; new postures are new vocabulary terms, never new kernel code.
The framework is agnostic about who the members are: members are accounts, and whether an account is an agent, a human, an organization or a contract is a property that eligibility modules establish, not one the kernel assumes. Non-principal elements — executable skills, datasets, tools, code, models, documents — are resources attached to a swarm: they hold no seat and receive no allocation, but a counterparty can verify exactly which versions the swarm works with. The framework is equally agnostic about value: it holds no funds and defines no allocation formula; the swarm account and the bound obligation settle, and the ledger supplies the attribution they need — as of the phase and charter version the work was done under, not as of whoever is a member later.
Illustrative flows (informative):
- A coordinator agent forms a swarm bound to a funded task tender; a council of three admits workers through a collective-decision adapter that snapshots the council as the roster of each decision; workers record contribution hashes; the coordinator submits the fulfillment from the swarm account; when the tender's judge accepts it, the tender adapter concludes the swarm
ACHIEVEDfrom the tender's own record, the reward sits in the swarm account and the coordinator pays the workers from it. - A conjecture is split into lemmas. Phase 0 is an open bidding phase: eligible bidders join and submit proof sketches within a two-day input window; an evaluation chamber carries two bidders into a closed execution phase as workers; one worker's eligibility lapses and anyone challenges it; the coordinator substitutes the third bidder into the closed phase; a verifier accepts each lemma as a milestone; the swarm concludes
ACHIEVEDwhen the last is accepted. - A standing committee answers queries for as long as its sponsor funds it; members roll in and out through an open boundary; a member whose identity goes stale is challenged and suspended by anyone; after a year the committee is succeeded by a new swarm under a revised specification and the old one records
SUPERSEDED.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
1. Terminology
- Swarm — a purpose-bound collective recorded in a swarm registry: a specification, a charter, a lifecycle state, a phase, a membership ledger, an objective, attached resources and decision-point authorities.
- Swarm Specification (spec) — the immutable, content-addressed set of parameters the registry executes. Identified by
specHash = keccak256(abi.encode(spec)). - Charter / Swarm Pack — the off-chain, versioned document set committed by
charterHash(Section 4). The charter is what a member accepts. - Charter version / commitment version — every amendment increments the charter version; a material amendment — one that changes the objective hash or the commitment hash, or that the authority declares material — also increments the commitment version. Members accept commitment versions.
- Initiator — the account that submitted the formation. Recorded for ever; holds no power unless the spec assigns it.
- Origin — what the formation came from: a single initiator, a joint initiation, a task's call for a swarm, a pre-authorized trigger, a decision, or a succession.
- Member — an account in the ledger with status
CANDIDATE,ACTIVE,SUSPENDEDorEXITED. "Active member" means statusACTIVE; a seat is one such record. - Role — a named seat class with minimum and maximum counts, an optional eligibility module and a self-join flag.
- Eligibility module — a contract answering whether an account qualifies for a role, consulted at admission and on challenge.
- Phase — one stage of the swarm's work with a declared collaboration posture, boundary and admission rule.
- Posture — the coordinate
(payoff, topology, coordination, boundary, admission)of a phase. Payoff, topology and coordination are terms of the registry's vocabulary. - Decision point — one of the sixteen named powers an authority may exercise (Section 11).
- Authority — who may exercise a decision point: a designated address, any holder of a role, any active member, or the initiator.
- Objective — what the swarm exists to attain: a descriptor commitment, a kind, an acceptance authority and an optional bound external obligation.
- Acceptance authority — the account or contract that records milestones and attainment: a verifier contract, a designated judge, a collective-decision adapter, an obligation adapter.
- Outcome type —
NONE,ACHIEVED,PARTIAL,FAILED,ABANDONEDorSUPERSEDED, recorded at conclusion and separate from the lifecycle state. - Resource — a non-principal element attached to a swarm: a kind, an optional on-chain anchor, a content digest with its algorithm, a provider and a transport hint.
- Swarm account — the account through which a swarm acts and holds value; bound to exactly one swarm.
- Revision — a per-swarm counter incremented by every state-changing call; the organizational change log.
- Fallback — a permissionless transition available after a declared duration (recruitment window, hard deadline, inactivity timeout, ineligible grace, settlement window).
2. Conformance
A conforming deployment is one ISwarmRegistry (Section 12) implementing ERC-165 and reporting the interface identifier of Section 16.
The following are REQUIRED of every conforming deployment: the specification data model and validation rules (Section 3), the base vocabulary (Section 7.1), charter commitment and acceptance (Section 4), the lifecycle state machine with its fallbacks and the revision log (Section 5), the membership ledger with two-sided admission, departure, suspension, removal and substitution (Section 6), single-phase operation with the posture declared (Section 7), the objective record and conclusion (Section 8), amendment with commitment versions and the exit window (Section 9), the authority model with ADDRESS and INITIATOR kinds (Section 11) and the clock disclosure (Section 15). This is the Minimal profile: a gated, single-phase swarm whose decision points are held by designated addresses or the initiator, with no eligibility modules, no contributions, no resources, no external obligation and no successor.
The following are normative-optional profiles; a deployment MAY reject specifications that use one it does not implement, and a deployment that accepts such a specification MUST implement the profile exactly as specified here: multi-phase operation — later phases with INHERIT, SELECT or RECRUIT admission and the ADVANCE decision point (Section 7.2); vocabulary extension (Section 7.1); eligibility modules and the challenge / expel path (Section 6.4); ROLE and ANY_MEMBER authorities (Section 11); signed charter acceptance (Section 4.3); contributions (Section 6.6); resources (Section 10); the swarm account (Section 13.2); succession and nesting (Section 9.2); the collective decision profile (Section 14.1); the agent identity profile (Section 14.2); the obligation binding profile (Section 14.3).
Section 17 names extension points that this ERC reserves without specifying. A deployment MUST NOT claim conformance to a reserved extension on the basis of this document.
3. Swarm Specification
3.1 Data model
library SAITypes {
enum Boundary { OPEN, GATED, CLOSED }
enum Admission { RECRUIT, INHERIT, SELECT }
enum AuthorityKind { NONE, ADDRESS, ROLE, ANY_MEMBER, INITIATOR }
enum DecisionPoint { ACTIVATE, ABORT, SUSPEND, RESUME, ADVANCE, CONCLUDE, DISSOLVE, SUCCEED,
ADMIT, REMOVE, REINSTATE, AMEND, ATTACH, DETACH, SET_ACCOUNT, REPRESENT }
enum Activation { ROSTER_MET, AUTHORITY, BOTH }
enum Contributions { DISABLED, MEMBERS, OPEN }
struct Authority { AuthorityKind kind; address target; bytes32 roleId; }
struct RoleSpec {
bytes32 roleId; // unique within the spec
uint32 min; // active members required for activation; 0 = none
uint32 max; // seats (active + suspended); 0 = unbounded
address eligibility; // IEligibility module; 0 = none
bool selfJoin; // may an eligible account join without the ADMIT authority
}
struct PhaseSpec {
bytes32 name;
bytes32 payoff; // registered term under keccak256("sai.dimension.payoff")
bytes32 topology; // registered term under keccak256("sai.dimension.topology")
bytes32 coordination; // registered term under keccak256("sai.dimension.coordination")
Boundary boundary;
Admission admission; // phase 0 MUST be RECRUIT
uint64 inputWindow; // seconds from phase start accepting admissions and contributions; 0 = unbounded
bytes32 descriptorHash; // informative
}
struct SwarmSpec {
RoleSpec[] roles;
PhaseSpec[] phases;
Authority[] authorities; // one per DecisionPoint, in enum order (16)
Activation activation;
Contributions contributions;
uint32 minMembers; // >= 1
uint32 maxMembers; // seats; 0 = unbounded
uint64 recruitmentWindow; // FORMING fallback; > 0
uint64 maxDuration; // hard cap from activation; 0 = none
uint64 inactivityTimeout; // 0 = disabled
uint64 ineligibleGrace;
uint64 settlementWindow;
uint64 amendmentExitWindow;
bytes32 descriptorHash; // informative
}
struct Origin { bytes32 kind; bytes32 ref; }
}specHash MUST equal keccak256(abi.encode(spec)) over the full SwarmSpec. Every field is part of the identifier; the two descriptorHash fields bind published descriptions to the parameters but MUST NOT influence execution.
3.2 Validation
form and succeed MUST reject a specification unless all of the following hold:
1 <= roles.length <= 32; role identifiers are pairwise distinct and non-zero; for every rolemax == 0 || min <= max; every non-zeroeligibilityaddress has code.1 <= phases.length <= 32;phases[0].admission == RECRUIT; every phase'spayoff,topologyandcoordinationis a registered term of its dimension (Section 7.1).authorities.length == 16. AnADDRESSauthority has a non-zerotargetand zeroroleId; aROLEauthority has zerotargetand aroleIdpresent inrolesother than the initiator role;NONE,ANY_MEMBERandINITIATORauthorities have zerotargetand zeroroleId.minMembers >= 1andmaxMembers == 0 || maxMembers >= minMembers.- If
activation != ROSTER_METthenauthorities[ACTIVATE].kind != NONE. - The forming state has a permissionless exit:
recruitmentWindow > 0(anABORTauthority only adds an earlier one). - The swarm has a liveness fallback:
maxDuration > 0 || inactivityTimeout > 0. - A conclusion is reachable:
authorities[CONCLUDE].kind != NONEor the objective'sacceptanceAuthorityis non-zero (Section 8). - If
phases.length > 1thenauthorities[ADVANCE].kind != NONE. - If a phase after the first has
admission == RECRUITandboundary == GATED, thenauthorities[ADMIT].kindisADDRESSorINITIATOR(aROLEorANY_MEMBERadmitter cannot exist in a roster that re-forms from nothing). - Every duration fits
2^63 - 1.
A deployment that does not implement a normative-optional profile MUST reject a specification that requires it (for example a non-zero eligibility, a ROLE authority, a contribution mode other than DISABLED, or more than one phase) rather than accept it and run it partially.
Reserved role identifiers. Role identifiers of the form keccak256("sai.role.<name>") with <name> in initiator, coordinator, worker, verifier, observer and bidder are reserved as conventions with the obvious meanings; a spec MAY use any other identifier. The initiator role is special: when roles contains keccak256("sai.role.initiator"), its min MUST be zero, admission to it MUST be refused, no ROLE authority may name it, and its eligibility module, if any, MUST be evaluated against msg.sender at form and succeed — the initiator is checked but never seated.
3.3 Immutability
A spec is frozen at form. There is no function that changes a role, a phase, the activation rule, the contribution mode or a duration. The holder of an ADDRESS authority MAY transfer that one slot (Section 11.3); the set of members holding a ROLE authority changes with the ledger. A structural change is a succession (Section 9.2): a new swarm under a new spec, and the old one concluded SUPERSEDED.
3.4 Origin
form records an Origin: kind is keccak256("sai.origin.<name>") with <name> in initiator (one account formed the swarm for itself), joint (a contract formed it for several initiators), task (a task's call for a swarm), trigger (a pre-authorized rule), decision (a collective decision) or succession (set by the registry itself); ref is an opaque reference the kind gives meaning to — a hash of the task reference, a decision issue identifier, a trigger rule hash, zero for a plain initiator. The registry records the origin and attaches no meaning to it. The initiator — the account that submitted the formation, which under joint, task or trigger is typically a contract — is recorded separately, and who holds organizational power is a third thing, fixed by the authorities. Three identities, three records.
4. Charter
4.1 Swarm Pack
The charter is an off-chain object graph whose root conforms to swarm-pack.schema.json. It carries the primary human-readable charter document (referenced by path and by the SHA-256 digest of its bytes), the objective descriptor (goal, preconditions, success criteria, milestones, acceptance, amendment rule), the commitment — the members' economic terms: the allocation rule per phase, costs, liabilities, data access and the actions taken in the members' name — the collaboration structure per phase, the roles with their eligibility in words, the resource manifest, an informative description of each decision-point authority, and prev, the previous version's charterHash (null at version 1). Digests inside the pack are written as sha256: followed by 64 lowercase hexadecimal characters; on-chain they are the same 32 bytes.
charterHash MUST equal the SHA-256 digest of the root object in the canonical form of RFC 8785 (JSON Canonicalization Scheme). objectiveHash MUST equal the SHA-256 digest of the objective member in the same canonical form, so that an objective can be cited independently of the pack that carries it; commitmentHash MUST equal the SHA-256 digest of the commitment member in the same canonical form, or be zero when the pack has no such member; and a phase's descriptorHash SHOULD equal the digest of the corresponding mode.phases member computed the same way; no digest is stored inside the member it covers. Because the registry compares the objective and commitment hashes at amendment (Section 9.1), the pack's structure, not the amending authority's judgment, decides which changes are material. A pack's specHash member MUST equal the on-chain specHash of the swarm it describes, and its version member the registry's charterVersion (1 at formation). Where the pack and the spec disagree, the spec is what runs; the pack describes, the spec executes.
charterURI is a transport hint. It MAY change without a version change (setCharterURI) and MUST NOT be used as a verification source.
4.2 Acceptance
Membership is acceptance of a charter version. An account becomes an active member only once it has accepted the swarm's current charter hash under the current commitment version (Section 6.2), and the ledger records, per member, the hash, the charter version and the commitment version most recently accepted. acceptCharter(swarmId, charterHash) MUST revert unless the caller holds a seat (CANDIDATE, ACTIVE or SUSPENDED) and charterHash equals the swarm's current charter hash; a join, applyToJoin or joinBySig call that names a stale hash MUST revert with the same error, so that no account is ever bound to a version it did not see. Every member-side act emits CharterAccepted with the hash and both versions accepted (signed true for joinBySig).
An agent that wants to join swarms meeting its own criteria without a transaction per swarm can pre-authorize them: a contract account whose ERC-1271 validation accepts Join digests (Section 4.3) matching its policy is, in this ERC's terms, an account that accepts charters automatically; the registry sees an acceptance either way.
4.3 Signed acceptance (normative-optional)
joinBySig(swarmId, account, roleId, charterHash, deadline, signature) MAY be called by anyone. The registry MUST verify the signature over the EIP-712 digest of
Join(bytes32 swarmId,address account,bytes32 roleId,bytes32 charterHash,uint256 seatEpoch,uint32 phase,uint256 deadline)under the domain EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) with name = "SAI", version = "1", the executing chain id and the registry address, where seatEpoch is the registry's current seatEpochOf(swarmId, account) — the number of seats of the account that have been closed explicitly (by leave, remove, expel or substitution) in this swarm so far — and phase is the swarm's current phase; signatures of externally owned accounts are verified by ecrecover with s in the lower half of the curve order and v in {27, 28}, signatures of contract accounts through ERC-1271. The call MUST revert after deadline. On success it has exactly the effect of join called by account, or of applyToJoin wherever join would be refused for the phase's boundary or the role's selfJoin. The digest binds the charter hash, so a signature for one version is invalid for the next; the seat epoch and the phase, so a signature relayed after the seat it opened has exited — explicitly, or lazily through a phase change — cannot seat the account again; it needs no further nonce because an account holds at most one seat per swarm and a second acceptance of the same version for the same seat changes nothing.
5. Lifecycle
5.1 States and outcome
enum LifecycleState { NONE, FORMING, ACTIVE, SUSPENDED, CONCLUDING, DISSOLVED }
enum OutcomeType { NONE, ACHIEVED, PARTIAL, FAILED, ABANDONED, SUPERSEDED }
enum Reason { NONE, AUTHORITY, ACCEPTANCE, RECRUITMENT_EXPIRED, TIMEOUT, INACTIVITY, ROSTER_LOST, SUCCESSION }The lifecycle state says whether the swarm is recruiting, working, paused, settling or gone. The outcome type says how its purpose ended. Settlement — whether and how value moved — is authoritative at the swarm account or the bound obligation and is not a registry state. A relying contract MUST read the dimension it needs: CONCLUDING or DISSOLVED for "no more work", outcome == ACHIEVED for "the purpose was attained", and its own records for "paid".
5.2 State machine
form ──► FORMING ──activate──► ACTIVE ◄──resume── SUSPENDED
│ │ │ ▲ ▲
│ abort / │ │ └──suspend──────┘ (authority; enforceRoster; enforceInactivity)
│ recruitmentWindow │ │
▼ │ └─enforceDeadline──► CONCLUDING(ABANDONED, TIMEOUT)
DISSOLVED(ABANDONED) └───conclude(outcome)─► CONCLUDING ──dissolve──► DISSOLVED
(also from SUSPENDED) (authority, or anyone after settlementWindow)| Transition | Caller | Condition |
|---|---|---|
→ FORMING | anyone (form) | spec valid (Section 3.2); objective valid (Section 8); initiator eligibility, if the spec declares an initiator role with a module, holds for msg.sender |
FORMING → ACTIVE | activate: anyone when activation == ROSTER_MET; the ACTIVATE authority otherwise | ROSTER_MET or BOTH: rosterMet is true (Section 6.5); AUTHORITY: no roster condition |
FORMING → DISSOLVED | abort: the ABORT authority at any time; anyone after formedAt + recruitmentWindow | outcome ABANDONED, reason AUTHORITY or RECRUITMENT_EXPIRED; SwarmConcluded then SwarmDissolved are emitted; every candidate and member is exited with reason SWARM_DISSOLVED |
ACTIVE → SUSPENDED | suspend: the SUSPEND authority; enforceRoster: anyone when rosterMet is false (Section 7.2 for the deferral after an advance); enforceInactivity: anyone when lastActivity + inactivityTimeout has passed and the timeout is non-zero | reason AUTHORITY, ROSTER_LOST or INACTIVITY; admissions, contributions, milestones and phase advances are refused while suspended; judgment on members (Section 6.3), leave, challenge, expel and amendment remain available |
SUSPENDED → ACTIVE | resume: the RESUME authority; anyone when the suspension reason was ROSTER_LOST and rosterMet is true again | — |
ACTIVE / SUSPENDED → CONCLUDING | conclude: the CONCLUDE authority or the objective's acceptance authority; enforceDeadline: anyone after hardDeadline; enforceInactivity: anyone when the swarm is already SUSPENDED and a further inactivityTimeout has passed since suspendedAt | conclude records the caller's outcome in {ACHIEVED, PARTIAL, FAILED, ABANDONED} with reason AUTHORITY or ACCEPTANCE; the fallbacks record ABANDONED with reason TIMEOUT or INACTIVITY |
ACTIVE / SUSPENDED → CONCLUDING | succeed: the SUCCEED authority | outcome SUPERSEDED, reason SUCCESSION; the successor is formed in the same transaction (Section 9.2) |
CONCLUDING → DISSOLVED | dissolve: the DISSOLVE authority; anyone after concludedAt + settlementWindow | every remaining candidate and member is exited with reason SWARM_DISSOLVED |
hardDeadline = activatedAt + maxDuration when maxDuration is non-zero and is otherwise unset. DISSOLVED is terminal: a dissolved swarm MUST NOT accept any transition, admission, contribution, amendment or resource change, and its outcome MUST NOT change. CONCLUDING MUST refuse admissions, contributions, milestones, amendments, phase advances, resource attachments, suspensions, removals, reinstatements, substitutions, expulsions and challenges; only leave remains, so the ledger can shrink by a member's own act and change in no other way while the account and the obligation settle against it.
Exits recorded for a whole roster at once — SWARM_DISSOLVED on abort and dissolve, NOT_CARRIED on an advance (Section 7.2) — MAY be derived lazily: an implementation MUST report them through memberStatus and MAY omit the per-member MemberExited events for them, so that a fallback never needs a loop over an unbounded roster.
Fallback calls are paths, not executors: someone must call them. Each MUST revert when its condition does not hold, and MUST succeed for any caller when it does.
5.3 Activity
The registry MUST record lastActivity for the swarm and for each member. Admission, acceptance, a contribution, a milestone, a phase advance, an amendment and a resource attachment are activity of the swarm; a contribution, acceptance or admission is activity of the member concerned. inactivityTimeout is measured from the swarm's lastActivity for the suspension stage and from suspendedAt for the conclusion stage (Section 5.2).
5.4 Revision log
Every state-changing call MUST increment the swarm's revision and emit Changed(swarmId, revision, selector, by) after its own events, where selector is the function selector of the call. Revisions are strictly increasing per swarm, so two changes to one swarm are totally ordered and every event of a transaction can be attributed to the revision it produced. A challenge that finds the predicate unevaluable changes nothing and emits no Changed.
6. Membership
6.1 States
enum MemberStatus { NONE, CANDIDATE, ACTIVE, SUSPENDED, EXITED }
enum SuspensionCause { NONE, AUTHORITY, INELIGIBLE }
enum ExitReason { NONE, LEFT, REMOVED, INELIGIBLE, NOT_CARRIED, REPLACED, SWARM_DISSOLVED }An account holds at most one seat in a swarm: one role, one status. A CANDIDATE has been admitted by the authority, or has applied or accepted, but not both. An ACTIVE member has both sides of admission. SUSPENDED keeps the seat and the role but not the standing; EXITED is terminal for that seat, with a reason, and a later re-admission of the same account opens a new seat with the earlier history retained in events.
6.2 Two-sided admission
Admission completes when both of the following have happened for the same account, role and current commitment version, in either order:
- the authority side: the
ADMITauthority calledadmit(swarmId, account, roleId); or, in a phase whose boundary isOPEN, for a role withselfJoin == true, the account's ownjoincounts as the authority side; - the member side: the account called
join,applyToJoinoracceptCharternaming the currentcharterHash, or a validjoinBySigwas relayed for it.
applyToJoin emits MemberApplied, admit emits MemberAdmitted, and every member-side act emits CharterAccepted (Section 4.2). On completion the seat becomes ACTIVE, admittedPhase and carriedPhase are set to the current phase and MemberActivated is emitted. Admission MUST be refused when: the swarm is not FORMING or ACTIVE; the current phase's boundary is CLOSED; the boundary is GATED and the call is join (candidates of a gated phase use applyToJoin and wait for admit); the boundary is OPEN and the role's selfJoin is false and the call is join; the phase's inputWindow has elapsed; the role is unknown or is the initiator role; the role's max is non-zero and already reached by the role's active and suspended seats; maxMembers is non-zero and already reached by active and suspended seats; the account already holds a seat that is not EXITED; or the role's eligibility module exists and does not return true (Section 6.4). Eligibility MUST be evaluated at the moment admission completes, whichever side completes it; an implementation MAY evaluate it additionally when the authority side is performed and refuse early. If the member side was recorded under an earlier commitment version, completion waits until the member accepts the current one (Section 9.1).
A CANDIDATE MAY be withdrawn by the account itself (leave) and MAY be dropped by the ADMIT authority (remove); neither is an exit of an active member, both close the seat, and both emit MemberExited with reason LEFT or REMOVED for traceability.
6.3 Departure, suspension, removal, substitution
leave(swarmId)MUST succeed for anyCANDIDATE,ACTIVEorSUSPENDEDaccount in a swarm that is notDISSOLVED, in every phase and state, and MUST setEXITED(LEFT). If the call falls within the exit window of a material amendment the member has not accepted (Section 9.1),MemberExitedMUST carryonAmendment = true. Consequences of leaving are outside this ERC.suspendMemberandremoveMUST be accepted only from theREMOVEauthority for anACTIVEorSUSPENDEDmember, settingSUSPENDED(AUTHORITY)orEXITED(REMOVED);removeof aCANDIDATEMUST be accepted only from theADMITauthority (dropping a candidate is the counterpart of admitting one).reinstateMUST be accepted only from theREINSTATEauthority, for aSUSPENDEDmember. If the cause wasINELIGIBLE, reinstatement MUST re-evaluate the role's eligibility module and MUST revert unless it returnstrue. This is reinstatement on judgment; a member suspended for ineligibility is also reinstated on evidence, by anyone'schallengeorexpel, once the module answerstrueagain (Section 6.4).substitute(swarmId, previous, next)MUST be accepted only from theADMITauthority, for anACTIVEorSUSPENDEDpreviousand anextthat holds no live seat and passes the role's eligibility. It MUST setprevioustoEXITED(REPLACED)and open a seat fornextin the same role with the authority side done andsubstitutedset;nextbecomesACTIVEonce it accepts the charter. Because the seat already exists, completing it is exempt from the boundary, input-window, role-maximum and member-maximum checks of Section 6.2 — a compromised worker can be replaced in aCLOSEDphase — and only the eligibility check applies. A substitution does not transfer contributions: attribution stays with the account that recorded them.suspendMember,remove,reinstate,substitute,challengeandexpelMUST be accepted only while the swarm isFORMING,ACTIVEorSUSPENDED(challengeandexpeladditionally require a role with a module, Section 6.4);leaveis accepted in every state butDISSOLVED.
6.4 Eligibility and challenge (normative-optional)
interface IEligibility {
function isEligible(bytes32 swarmId, bytes32 roleId, address account) external view returns (bool);
}A role MAY bind one module. At admission the registry MUST call it and MUST refuse the admission unless the call returns true without reverting. challenge(swarmId, account) MAY be called by anyone for an ACTIVE member, or for a member SUSPENDED(INELIGIBLE), of a role with a module. The registry MUST call the module and classify the call as evaluated, with the module's answer, or unevaluable: a revert, a return that is not a boolean, and a false returned with less than one eighth of the gas available before the call still remaining are all unevaluable — an unevaluable predicate is not a failed one. An evaluated false on an ACTIVE member MUST set it SUSPENDED(INELIGIBLE) and emit MemberSuspended; an evaluated true on a member SUSPENDED(INELIGIBLE) MUST reinstate it and emit MemberReinstated; every other evaluated answer changes nothing; an unevaluable call changes nothing and MUST NOT revert on that account. challenge returns (evaluated, eligible): (true, false) and (true, true) for the two answers, (false, false) when unevaluable. Challenged MUST be emitted in the evaluated cases with the module's answer.
The gas rule exists because a module commonly wraps its own reads in a catch and answers false when one fails; under the all-but-one-sixty-fourth rule a caller can then pick a gas limit at which the module survives but its read does not, and turn an honest member's standing into false. The module cannot defend against its caller's gas; the registry can, once, for every module: a false that arrives with almost nothing left is not an answer. A true needs no such guard, and a challenger who means the question supplies ample gas.
expel(swarmId, account) MAY be called by anyone once a member has been SUSPENDED(INELIGIBLE) for longer than ineligibleGrace. It MUST re-evaluate the module: an evaluated false sets EXITED(INELIGIBLE); an evaluated true reinstates the member instead, exactly as challenge would; an unevaluable call changes nothing and MUST revert with SAI_Unevaluable, so that the caller knows nothing was decided. Expulsion therefore never rests on the clock alone: the grace period opens the question, the module answers it, and whoever asks — a counterparty, another member, the member itself — re-adjudicates on current evidence. A member suspended for ineligibility MAY in addition be reinstated by the REINSTATE authority (Section 6.3).
Modules are expected to read trust-assertion registries (an assertion of at least a level under a scheme from listed issuers), agent identity registries (an address-anchored identity in its active state), identity-and-reputation registries (Section 14.2), allowlists, token holdings, or compositions of these. This ERC attaches no meaning to how a module decides; a module's address is part of the hashed spec, and candidates read it before accepting the charter. One rule binds every module, because the registry reads it at the instant of a transition — admission, carrying into a phase, challenge, expulsion — and acts on that instant: a module MUST NOT answer from a quantity the account under evaluation can change within the same transaction. A module that gates on a token holding reads a checkpointed balance at an earlier block (as ERC-5805 getPastVotes provides), a time-weighted holding or a holding that has lasted a declared period, never the live balance; a borrowed balance passes the live read and is gone before the block ends, and the registry cannot tell it from an owned one. Agents malfunction, lose their keys, change controllers, retire or drift from the behaviour they were vetted for; a module is where evidence of any of these — an identity gone stale, an assertion revoked, a wallet moved — becomes a verdict the kernel can act on, and the kernel's part is the same for all of them: suspend on challenge, expel after grace, reinstate on recovery, substitute a successor.
6.5 Roster condition
rosterMet(swarmId) MUST return true iff the number of ACTIVE members is at least minMembers and, for every role, at least the role's min. Only ACTIVE members count: suspended members hold seats but not standing.
6.6 Contributions (normative-optional)
The spec's contributions mode fixes who may record. recordContribution(swarmId, contributionHash, uri) MUST revert under DISABLED; under MEMBERS it MAY be called by an ACTIVE or SUSPENDED member; under OPEN by anyone — an external party may contribute a lemma, a fix or a dataset without becoming a member. It MUST be accepted only while the swarm is ACTIVE and the current phase's inputWindow is open, MUST emit Contribution with the current phase, the current charter version, isMember and whileSuspended, MUST update lastActivity (the member's as well when the caller is one), and MUST NOT store the contribution: the event stream is the attribution record. The registry attaches no meaning to a contribution; allocation profiles and reputation bridges read the stream and apply their own rules, the flags let them exclude work recorded without standing or by non-members, and the phase and charter version let them pay for work under the rules it was done under rather than under the roster of a later moment.
7. Phases and posture
7.1 Posture and vocabulary
Each phase declares its collaboration posture as the coordinate (payoff, topology, coordination, boundary, admission):
| Dimension | Question | Executed? | Values |
|---|---|---|---|
payoff | How does outcome value map to members? | declared | base terms shared — members in good standing share by the charter's allocation rule; exclusive — a ranked or selected strict subset is rewarded; mixed — a shared component plus a ranked or conditional component |
topology | How is the objective decomposed? | declared | base terms redundant — the same work by several members; partitioned — disjoint parts; sequential — hand-offs; hierarchical — a coordinator decomposes and delegates, including to child swarms; iterative — propose, critique, revise; free — no declared structure |
coordination | Who steers? | declared | base terms leader — a designated coordinator; collective — a collective decision policy (Section 14.1); market — internal bidding or pricing; automated — verifier- and rule-driven; none — self-organized |
boundary | Who may enter during the phase? | executed | OPEN, GATED, CLOSED (Section 6.2) |
admission | How is the phase's roster formed from the previous one? | executed | RECRUIT — from scratch; INHERIT — every member carries over; SELECT — the ADVANCE authority names who carries over |
The three declared dimensions are vocabulary terms: bytes32 identifiers registered in the registry under a dimension identifier keccak256("sai.dimension.<dimension>"). The base terms, keccak256("sai.<dimension>.<name>") for the names above, MUST be registered by every conforming registry at deployment. registerTerm(dimension, term, uri) MAY be called by anyone, for one of the three dimensions, with a non-zero term not yet registered; it records the term with a descriptor URI and emits TermRegistered. Registration is write-once: a term's meaning is fixed by its first descriptor. A spec MUST use registered terms (Section 3.2); a term unknown to the registry is a spec error, not a silent default, so that a counterparty reading a posture can always resolve it to a published description.
The posture is a declaration: boundary and admission are executed by the registry; payoff, topology and coordination are read by counterparties, allocation profiles and indexers and MUST be consistent with the charter's description of the phase, which the registry cannot check. Named modes are trajectories in this space (informative): cooperation — shared × partitioned or sequential, one phase; competition — exclusive × redundant × automated or leader, one phase; a contest with a shared purse — shared × redundant, several solving paths and one reward pool; co-opetition — a parent swarm exclusive × hierarchical over child swarms that are shared inside; tender — an OPEN, exclusive × redundant bidding phase followed, through SELECT, by a CLOSED, shared × partitioned execution phase; staged elimination — repeated exclusive × redundant phases each entered through SELECT; debate — iterative × collective; ensemble — shared × redundant × automated; pipeline — shared × sequential; a standing committee — OPEN boundary with a STANDING objective. What a phase eliminates is a separate fact from how it is declared: SELECT acts on membership; a proposal that is not chosen is a contribution-level fact that exits nobody by itself, and a phase that keeps every member while choosing among their proposals uses INHERIT.
Posture applies per phase. Finer structure — a sub-group that shares while another competes, a role that works in a pipeline inside a partitioned phase — is expressed by child swarms (Section 9.2), each with its own posture, under a parent whose charter says how their results compose.
7.2 Advancing (normative-optional)
advance(swarmId, carried, roles) MUST be accepted only from the ADVANCE authority, only while the swarm is ACTIVE, and only while phase + 1 < phases.length. Let next = phases[phase + 1]:
INHERIT:carriedandrolesMUST be empty; everyACTIVEandSUSPENDEDmember keeps its seat, role and status.SELECT:carriedlistsACTIVEmembers, pairwise distinct;rolesis empty or of equal length and names, per carried member, the role it holds in the new phase (its eligibility module, if any, MUST returntrue; the resulting roster MUST respect every role'smaxandmaxMembers). Listed members keepACTIVEwithcarriedPhaseupdated andMemberCarriedemitted for each, with its role in the new phase; every otherACTIVE,SUSPENDEDorCANDIDATEaccount is exited with reasonNOT_CARRIED.RECRUIT:carriedandrolesMUST be empty; every account is exited with reasonNOT_CARRIEDand the roster re-forms under the new phase's boundary.
The phase index increments, phaseStartedAt is set, and PhaseAdvanced is emitted with the number of seats carried (every active and suspended seat under INHERIT, the listed members under SELECT, zero under RECRUIT). The roster thresholds of Section 6.5 apply at activation; after a RECRUIT or SELECT advance, enforceRoster MUST NOT be accepted before phaseStartedAt + recruitmentWindow, so that a legitimately re-forming roster is not suspended at once. An implementation MAY evaluate NOT_CARRIED lazily from carriedPhase and the last phase entered through SELECT or RECRUIT, provided memberStatus reports it (Section 5.2).
8. Objective and attainment
enum ObjectiveKind { TERMINAL, MILESTONED, EXPLORATORY, STANDING }
enum MilestoneStatus { PENDING, ACCEPTED, REJECTED }
struct ExternalRef { uint256 chainId; address target; uint256 id; }
struct ObjectiveInput { bytes32 objectiveHash; ObjectiveKind kind; address acceptanceAuthority;
ExternalRef obligation; uint32 milestoneCount; }form MUST reject an objective unless objectiveHash is non-zero, milestoneCount > 0 iff kind == MILESTONED, and — when authorities[CONCLUDE].kind == NONE — acceptanceAuthority is non-zero. The acceptance authority is the account that records attainment: a verifier contract for mechanically checkable objectives, a designated judge, a collective-decision adapter (Section 14.1), or an adapter reading the bound obligation (Section 14.3). The obligation binds the objective to an external duty; it is a reference the registry never calls, and a swarm needs none to exist: a group may form around a purpose before any task is published, and bind or spawn tasks later. Objective.version is the charter version at which objectiveHash was last set: 1 at formation, and the new charter version whenever an amendment changes the objective hash (Section 9.1).
The four kinds declare how attainment is read. TERMINAL: one deliverable, one conclusion. MILESTONED: a sequence of milestones, each accepted or rejected by the acceptance authority. EXPLORATORY: a search for a result under agreed resources — ACHIEVED means the result was found and accepted, PARTIAL means the best known result or an unresolved report was delivered, FAILED means the objective was shown unattainable, and ABANDONED means the search stopped without a conclusion; a program that ends has not thereby proved a proposition false, so an exploratory swarm that runs out of resources concludes PARTIAL or ABANDONED, never FAILED, unless its acceptance authority has established infeasibility. STANDING: recurring or open-ended delivery that ends by decision, succession or a fallback.
recordMilestone(swarmId, index, status, resultHash) MUST be accepted only from the acceptance authority, only while the swarm is ACTIVE, only for index < milestoneCount, only with status != PENDING, and only for a milestone not already ACCEPTED; an ACCEPTED milestone is final, a REJECTED one MAY be recorded again. milestonesAccepted counts accepted milestones.
conclude(swarmId, outcome, resultHash) MUST be accepted from the CONCLUDE authority or from the acceptance authority, while the swarm is ACTIVE or SUSPENDED, with outcome in {ACHIEVED, PARTIAL, FAILED, ABANDONED}. For a MILESTONED objective the registry MUST refuse ACHIEVED unless every milestone is ACCEPTED, and MUST refuse PARTIAL unless at least one is. The reason is ACCEPTANCE when the caller is the acceptance authority and AUTHORITY otherwise. The outcome and resultHash are frozen at conclusion. Whether the swarm's own conclusion has any effect outside the swarm is a separate fact: a bound tender accepts or rejects under its own rules, and only a tender that has itself committed to the swarm's acceptance mechanism is bound by it.
9. Amendment and succession
9.1 Amendment
amend(swarmId, charterHash, charterURI, objectiveHash, commitmentHash, material) MUST be accepted only from the AMEND authority, only while the swarm is FORMING, ACTIVE or SUSPENDED, with a non-zero charterHash different from the current one; objectiveHash and commitmentHash MAY each be zero or unchanged, meaning "not changed". It increments charterVersion, records the new hash and URI and lastAmendedAt, and, for each of objectiveHash and commitmentHash that is non-zero and differs, records the new value (the objective's version becomes the new charter version). Two classes of amendment exist, and the registry decides which one it is:
- a minor amendment leaves the objective hash and the commitment hash unchanged and is not declared material: re-ordering sub-tasks, re-balancing work inside the budget, correcting the text. It changes the charter version only; existing members remain accepted for the current commitment version and no exit window opens.
- a material amendment changes the objective hash or the commitment hash — a changed goal, allocation, cost, liability, data access or outside action in the members' name is a changed
commitmentorobjectivemember by construction (Section 4.1) — or is declared material by the authority (material == true) although neither hash changed. It also incrementscommitmentVersionand opens an exit window ofamendmentExitWindowseconds (exitWindowUntil).
The material flag is therefore one-way: an authority can declare a textual change material and can never declare a change of objective or commitment minor. The registry compares hashes and never reads charter text; which changes are structural is fixed by the pack schema, and a charter that keeps economic terms outside its commitment member has put them where the registry cannot protect them, visibly to every reader. CharterAmended carries both versions, the class as enacted, the three hashes and the window end. Amending changes neither the spec nor the objective's kind, acceptance authority, obligation or milestone count — those belong to the spec and to succession.
A member's recorded acceptance does not change on amendment. Until the member calls acceptCharter with the new hash, acceptedCharterOf returns the versions it last accepted; within the exit window a leave by a member that has not accepted the current commitment version is flagged onAmendment (Section 6.3). A candidate whose member side was recorded under an earlier commitment version does not complete admission until it accepts the current one. The registry MUST NOT suspend or exit a member for not accepting an amendment: what non-acceptance means — exclusion from allocation under the new version, or deemed acceptance after the window — is the charter's rule, and allocation profiles read acceptedCharterOf to apply it.
setCharterURI MUST be accepted only from the AMEND authority, in any state but DISSOLVED, changes the transport hint alone and emits CharterURIUpdated.
9.2 Succession and nesting (normative-optional)
succeed(swarmId, newSpec, charterHash, commitmentHash, charterURI, objective, carried, roles) MUST be accepted only from the SUCCEED authority while the swarm is ACTIVE or SUSPENDED. It forms a successor swarm in state FORMING with predecessorId = swarmId, the predecessor's parentId, origin kind keccak256("sai.origin.succession") with the predecessor's identifier as reference, and the caller as initiator; records successorId on the predecessor; and concludes it SUPERSEDED with reason SUCCESSION. carried lists ACTIVE members of the predecessor and roles, of equal length, their roles in the successor; each is opened in the successor as a CANDIDATE with the authority side done and becomes ACTIVE once it accepts the successor's charter (two-sided admission is preserved across a succession). The successor's spec and objective are validated as in Sections 3.2 and 8.
A swarm MAY name a parentId at formation; the registry MUST require that the parent exists and is FORMING or ACTIVE, and records the relation. Nothing else is implied: a parent's conclusion does not conclude its children, a child with its own objective and obligation outlives its parent, and a child's conclusion is a milestone or contribution of the parent only if the parent's charter says so. Parent and child are separate swarms with separate ledgers, and one account may hold a seat in each with distinct roles, acceptances and powers.
10. Resources (normative-optional)
struct ResourceInput { bytes32 kind; uint256 chainId; address target; uint256 id;
bytes32 digest; bytes32 digestKind; string uri; bytes32 roleId; address provider; }A resource is a non-principal element of a swarm. attach MUST be accepted only from the ATTACH authority while the swarm is FORMING or ACTIVE, with a non-zero kind and digest and a roleId that is zero or present in the spec; it assigns resourceId = keccak256(abi.encode(swarmId, counter)), records the resource with attachedBy = msg.sender, and emits ResourceAttached. detach MUST be accepted only from the DETACH authority for an attached resource in any state but DISSOLVED and marks it detached; the record is retained. A new version of a resource is a new attachment, so the digest a phase worked with stays readable after the resource moved on.
digest commits to the resource's content and digestKind says how it was computed: keccak256("sha-256") for a SHA-256 of the content, keccak256("keccak256") for a Keccak-256, or a kind an adapter defines for what its anchored contracts report (a token-bound executable skill reports its package digest under the algorithm its own standard fixes). The registry compares, never computes. provider names who provides or manages the resource; it may differ from attachedBy and from the anchor's controller, and the four relations — member, user of a resource, controller of a resource, beneficiary of a resource — stay distinct: joining a swarm grants the use of nothing; use comes from an attachment scoped to one's role.
Resources hold no seat, pass no eligibility check and receive no allocation; their controllers may be members or beneficiaries under the charter, and a metered resource (an executable skill billed per call by its own contract) is paid through its own mechanism, typically by the swarm account. Kinds are keccak256("sai.resource.<name>"); this ERC defines skill, dataset, tool, code, model, document, oracle and fund, and a deployment MUST accept kinds it does not know. A deployment MAY bind an IResourceAdapter per kind (Section 13.3), write-once at deployment, and then MUST, at attach, require exists to be true and digestOf to equal the committed (digest, digestKind) for resources of that kind with an anchor.
11. Authorities
11.1 Resolution
isAuthorized(swarmId, decisionPoint, account) MUST return true iff the spec's authority for the decision point is: ADDRESS and account == target as currently held; ROLE and account is an ACTIVE member holding roleId; ANY_MEMBER and account is an ACTIVE member; INITIATOR and account is the swarm's initiator. NONE authorizes nobody. In a DISSOLVED swarm isAuthorized MUST return false for every decision point and every kind of authority: an ADDRESS or INITIATOR holder is no more authorized after dissolution than a role holder whose seat has exited, so that a contract reading the registry for authority — the swarm account above all — gets one answer for every kind. Every transition attributed to an authority in Sections 5 to 10 MUST revert unless isAuthorized holds for msg.sender, and the fallbacks of Section 5 are available to anyone regardless of the authorities.
REPRESENT is the one decision point the registry itself never executes: it names who may act for the swarm through its account — submit a deliverable, sign, pay, hold — and the swarm account (Section 13.2) reads it. A spec that leaves it NONE has a swarm nobody can speak for.
11.2 Two authorities, one transition
conclude is accepted from the CONCLUDE authority or from the acceptance authority (Section 8); remove of a CANDIDATE is accepted from the ADMIT authority instead of the REMOVE authority (Section 6.3); activate is accepted from anyone under ROSTER_MET; resume is accepted from anyone after ROSTER_LOST once the roster holds; abort, dissolve, enforceRoster, enforceInactivity, enforceDeadline, challenge and expel are accepted from anyone once their conditions hold (Sections 5 and 6.4). No other transition has a second path.
11.3 Transfer
transferAuthority(swarmId, decisionPoint, newAuthority) MUST be accepted only from the current target of an ADDRESS authority, with a non-zero newAuthority, in any state but DISSOLVED. It changes the held address for that one slot and emits AuthorityTransferred. The spec is unchanged: specOf keeps reporting the address at formation, and authorityOf reports the current holder. Authorities of other kinds are not transferable; they follow the ledger or the initiator record.
12. Registry interface
interface ISwarmRegistry /* is IERC165, IERC6372 */ {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
event SwarmFormed(bytes32 indexed swarmId, address indexed initiator, bytes32 indexed parentId,
bytes32 specHash, bytes32 charterHash, bytes32 commitmentHash, string charterURI,
bytes32 originKind, bytes32 originRef, bytes32 predecessorId);
event SwarmActivated(bytes32 indexed swarmId, uint64 at, uint64 hardDeadline, address by);
event SwarmSuspended(bytes32 indexed swarmId, SAITypes.Reason reason, address by);
event SwarmResumed(bytes32 indexed swarmId, address by);
event PhaseAdvanced(bytes32 indexed swarmId, uint32 indexed phase, uint32 carried, address by);
event SwarmConcluded(bytes32 indexed swarmId, SAITypes.OutcomeType outcome, SAITypes.Reason reason, bytes32 resultHash, address by);
event SwarmDissolved(bytes32 indexed swarmId, uint64 at, address by);
event SwarmSucceeded(bytes32 indexed swarmId, bytes32 indexed successorId, address by);
event CharterAmended(bytes32 indexed swarmId, uint32 indexed charterVersion, uint32 commitmentVersion, bool material,
bytes32 charterHash, bytes32 objectiveHash, bytes32 commitmentHash, string charterURI,
uint64 exitUntil, address by);
event CharterURIUpdated(bytes32 indexed swarmId, string charterURI);
event AuthorityTransferred(bytes32 indexed swarmId, SAITypes.DecisionPoint indexed decisionPoint,
address indexed previous, address next);
event AccountSet(bytes32 indexed swarmId, address account, address by);
event Changed(bytes32 indexed swarmId, uint64 indexed revision, bytes4 selector, address by);
event MemberApplied(bytes32 indexed swarmId, address indexed account, bytes32 indexed roleId, bytes32 charterHash);
event MemberAdmitted(bytes32 indexed swarmId, address indexed account, bytes32 indexed roleId, address by);
event CharterAccepted(bytes32 indexed swarmId, address indexed account, bytes32 charterHash,
uint32 charterVersion, uint32 commitmentVersion, bool signed);
event MemberActivated(bytes32 indexed swarmId, address indexed account, bytes32 indexed roleId, uint32 phase);
event MemberSuspended(bytes32 indexed swarmId, address indexed account, SAITypes.SuspensionCause cause, address by);
event MemberReinstated(bytes32 indexed swarmId, address indexed account, address by);
event MemberExited(bytes32 indexed swarmId, address indexed account, SAITypes.ExitReason reason, bool onAmendment, address by);
event MemberSubstituted(bytes32 indexed swarmId, address indexed previous, address indexed next, bytes32 roleId, address by);
event MemberCarried(bytes32 indexed swarmId, address indexed account, bytes32 indexed roleId, uint32 phase);
event Challenged(bytes32 indexed swarmId, address indexed account, address indexed by, bool eligible);
event Contribution(bytes32 indexed swarmId, address indexed contributor, uint32 indexed phase,
bytes32 contributionHash, string uri, uint32 charterVersion, bool isMember, bool whileSuspended);
event MilestoneRecorded(bytes32 indexed swarmId, uint32 indexed index, SAITypes.MilestoneStatus status,
bytes32 resultHash, address by);
event ResourceAttached(bytes32 indexed swarmId, bytes32 indexed resourceId, bytes32 indexed kind,
uint256 chainId, address target, uint256 id, bytes32 digest, bytes32 digestKind,
string uri, bytes32 roleId, address provider, address by);
event ResourceDetached(bytes32 indexed swarmId, bytes32 indexed resourceId, address by);
event TermRegistered(bytes32 indexed dimension, bytes32 indexed term, string uri, address by);
function specHashOf(SAITypes.SwarmSpec calldata spec) external pure returns (bytes32);
function form(SAITypes.SwarmSpec calldata spec, bytes32 charterHash, bytes32 commitmentHash, string calldata charterURI,
bytes32 parentId, SAITypes.Origin calldata origin, SAITypes.ObjectiveInput calldata objective)
external returns (bytes32 swarmId);
function activate(bytes32 swarmId) external;
function abort(bytes32 swarmId) external;
function suspend(bytes32 swarmId) external;
function resume(bytes32 swarmId) external;
function enforceRoster(bytes32 swarmId) external;
function enforceInactivity(bytes32 swarmId) external;
function enforceDeadline(bytes32 swarmId) external;
function advance(bytes32 swarmId, address[] calldata carried, bytes32[] calldata roles) external;
function conclude(bytes32 swarmId, SAITypes.OutcomeType outcome, bytes32 resultHash) external;
function dissolve(bytes32 swarmId) external;
function succeed(bytes32 swarmId, SAITypes.SwarmSpec calldata newSpec, bytes32 charterHash, bytes32 commitmentHash,
string calldata charterURI, SAITypes.ObjectiveInput calldata objective,
address[] calldata carried, bytes32[] calldata roles)
external returns (bytes32 successorId);
function amend(bytes32 swarmId, bytes32 charterHash, string calldata charterURI, bytes32 objectiveHash,
bytes32 commitmentHash, bool material) external;
function setCharterURI(bytes32 swarmId, string calldata charterURI) external;
function transferAuthority(bytes32 swarmId, SAITypes.DecisionPoint decisionPoint, address newAuthority) external;
function setAccount(bytes32 swarmId, address account) external;
function applyToJoin(bytes32 swarmId, bytes32 roleId, bytes32 charterHash) external;
function join(bytes32 swarmId, bytes32 roleId, bytes32 charterHash) external;
function joinBySig(bytes32 swarmId, address account, bytes32 roleId, bytes32 charterHash,
uint256 deadline, bytes calldata signature) external;
function acceptCharter(bytes32 swarmId, bytes32 charterHash) external;
function admit(bytes32 swarmId, address account, bytes32 roleId) external;
function leave(bytes32 swarmId) external;
function suspendMember(bytes32 swarmId, address account) external;
function reinstate(bytes32 swarmId, address account) external;
function remove(bytes32 swarmId, address account) external;
function challenge(bytes32 swarmId, address account) external returns (bool evaluated, bool eligible);
function expel(bytes32 swarmId, address account) external;
function substitute(bytes32 swarmId, address previous, address next) external;
function recordContribution(bytes32 swarmId, bytes32 contributionHash, string calldata uri) external;
function recordMilestone(bytes32 swarmId, uint32 index, SAITypes.MilestoneStatus status, bytes32 resultHash) external;
function attach(bytes32 swarmId, SAITypes.ResourceInput calldata resource) external returns (bytes32 resourceId);
function detach(bytes32 swarmId, bytes32 resourceId) external;
function registerTerm(bytes32 dimension, bytes32 term, string calldata uri) external;
function getSwarm(bytes32 swarmId) external view returns (SAITypes.Swarm memory);
function specOf(bytes32 swarmId) external view returns (SAITypes.SwarmSpec memory);
function phaseSpecOf(bytes32 swarmId, uint32 phase) external view returns (SAITypes.PhaseSpec memory);
function roleSpecOf(bytes32 swarmId, bytes32 roleId) external view returns (SAITypes.RoleSpec memory);
function authorityOf(bytes32 swarmId, SAITypes.DecisionPoint decisionPoint) external view returns (SAITypes.Authority memory);
function isAuthorized(bytes32 swarmId, SAITypes.DecisionPoint decisionPoint, address account) external view returns (bool);
function objectiveOf(bytes32 swarmId) external view returns (SAITypes.Objective memory);
function milestoneOf(bytes32 swarmId, uint32 index) external view returns (SAITypes.MilestoneStatus status, bytes32 resultHash, uint64 at);
function getMember(bytes32 swarmId, address account) external view returns (SAITypes.Member memory);
function memberStatus(bytes32 swarmId, address account) external view returns (SAITypes.MemberStatus status, SAITypes.ExitReason exitReason);
function memberCount(bytes32 swarmId) external view returns (uint32 active, uint32 suspended, uint32 candidates);
function roleCount(bytes32 swarmId, bytes32 roleId) external view returns (uint32 active, uint32 suspended);
function rosterMet(bytes32 swarmId) external view returns (bool);
function acceptedCharterOf(bytes32 swarmId, address account) external view
returns (bytes32 charterHash, uint32 charterVersion, uint32 commitmentVersion);
function seatEpochOf(bytes32 swarmId, address account) external view returns (uint256);
function getResource(bytes32 swarmId, bytes32 resourceId) external view returns (SAITypes.Resource memory);
function resourceCount(bytes32 swarmId) external view returns (uint256);
function resourceAt(bytes32 swarmId, uint256 index) external view returns (bytes32 resourceId);
function resourceAdapterOf(bytes32 kind) external view returns (address);
function termURI(bytes32 dimension, bytes32 term) external view returns (string memory);
function isTerm(bytes32 dimension, bytes32 term) external view returns (bool);
function hardDeadlineOf(bytes32 swarmId) external view returns (uint64);
function inputOpen(bytes32 swarmId) external view returns (bool);
function joinDigest(bytes32 swarmId, address account, bytes32 roleId, bytes32 charterHash, uint256 deadline)
external view returns (bytes32);
function clock() external view returns (uint48);
function CLOCK_MODE() external view returns (string memory);
}swarmId MUST be unique within the registry instance; the reference implementation derives it from the chain id, the registry address and a counter. inputOpen MUST return true while the swarm is FORMING (the recruitment window alone bounds recruiting) and, while it is ACTIVE, whether the current phase's inputWindow has not elapsed since phaseStartedAt (phase 0 starts at activation; an unbounded window never elapses); hardDeadlineOf returns zero when maxDuration is zero or the swarm has not been activated; memberCount and roleCount count seats as the lazy rules of Section 5.2 report them. The full SAITypes library, including the Swarm, Member, Objective and Resource records returned by the views, is given in SAITypes.sol. The registry MUST store the full specification so that every parameter it executes is readable through specOf, phaseSpecOf, roleSpecOf and authorityOf.
13. Auxiliary interfaces
13.1 Eligibility module
IEligibility (Section 6.4) is implemented by modules, not by the registry.
13.2 Swarm account (normative-optional)
interface ISwarmAccount {
function swarmId() external view returns (bytes32);
function registry() external view returns (address);
function recoveryAddress() external view returns (address);
}setAccount(swarmId, account) MUST be accepted only from the SET_ACCOUNT authority, only while the swarm is FORMING or ACTIVE, only once per swarm, and only for an account with code whose swarmId() equals swarmId and whose registry() equals the registry. The account is the address a counterparty deals with — the fulfiller of record on a tender, the holder of attached assets, the payer of allocations. An account conforming to this profile MUST let only accounts for which the registry currently reports isAuthorized(swarmId, REPRESENT, caller) act through it by representation; because the registry authorizes nobody in a dissolved swarm (Section 11.1), representation ends with the swarm for every kind of REPRESENT authority. What then happens to what the account holds is the account's own post-dissolution rule, and a conforming account MUST state it on-chain: recoveryAddress() MUST return the one account that may act through it once the swarm is DISSOLVED, or zero when nobody may and whatever is left stays. The rule MUST NOT depend on membership state or on the kind of the REPRESENT authority, MUST be changeable only by an account authorized for REPRESENT and only while the swarm is not DISSOLVED, and is frozen with the swarm. Two swarms on the same account code thus answer "who can recover what is left" the same way, and a counterparty reads the answer before funding. The reference account takes the recovery address at deployment, lets a representative change it while the swarm lives, and after dissolution accepts calls from that address alone (recover). How the account executes calls and distributes value is otherwise outside this ERC. The account is expected to read the ledger (memberStatus, acceptedCharterOf, the Contribution stream with its phase and charter version) and the phase's declared payoff, to pay for work under the roster and rules of the phase it was done in, and to refuse to pay an account that was not a member in good standing then.
13.3 Resource adapter (normative-optional)
interface IResourceAdapter {
function kind() external view returns (bytes32);
function exists(uint256 chainId, address target, uint256 id) external view returns (bool);
function controllerOf(uint256 chainId, address target, uint256 id) external view returns (address);
function digestOf(uint256 chainId, address target, uint256 id) external view returns (bytes32 digest, bytes32 digestKind);
}Adapters MUST NOT revert from these views; a missing anchor is reported as exists == false, a zero controller and zero digests. A deployment that binds adapters publishes their addresses with the instance; resourceAdapterOf(kind) reports them.
14. Profiles
14.1 Collective decision profile
A swarm whose steering is collective holds one or more decision points through a decision adapter: a contract that is the ADDRESS authority of those points and is itself a relying contract of an ERC-8436 registry. A conforming adapter MUST follow the relying-contract pattern of that ERC:
- it MUST verify, before filing, that it is the registry's current authority for the decision point of the swarm it files for;
- a proposal is the exact registry calldata to perform on a Yes; the adapter MUST accept only calls of the functions the decision point may exercise (for
ADMIT:admit,substitute,removeof a candidate; forREMOVE:remove,suspendMember; forAMEND:amend,setCharterURI; and the one function of every other point), whose first argument is the swarm; - the roster of a decision is the set of active members of a deciding role at the moment of filing: the adapter MUST verify that the supplied roster is exactly that set (every entry an active member of the role, pairwise distinct, as many as the registry counts), and MUST register the roster as a fixed-roster K-of-N policy of its own policy family for the swarm and decision point — one version per decision, so that members admitted or removed after the snapshot take part in later decisions and never in the one under way;
- it files as the consumer (
CONSUMER_FILED) with subject{chainId, target = the swarm registry, id = uint256(swarmId), dataHash = keccak256(abi.encode(decisionPoint, call, charterVersion, phase))}, questionkeccak256("sai.dp.<name>")for the decision point (activate,abort,suspend,resume,advance,conclude,dissolve,succeed,admit,remove,reinstate,amend,attach,detach,set-account,represent), effectskeccak256("sai.enact")andkeccak256("sai.refuse"), dispositionkeccak256("sai.status-quo")andobligationId = keccak256(abi.encode(swarmId, decisionPoint, call, phase)), so that at most one unfinished binding proceeding exists per concrete proposal, a proposal being a call in a phase; consumerDeadlineis the earlier of the swarm's hard deadline and the current phase's input window end when that end is still ahead, minus an execution margin, and zero when neither bounds the proposal;- on a
Finalresult with outcome typeDecidedandoutcomeYes == trueit performs the recorded call on the registry, as the authority, unless the swarm's commitment version or phase has changed since filing — a decision is taken under one set of commitments and in one phase and is performed under no other;advancein particular is relative, so a Yes obtained in an earlier phase would perform a transition nobody voted on — and records the enactment (Enacted, orFailedwhen the registry refused — the decision stands and may be retried); onDecidedwithoutcomeYes == falseit records that nothing is to be done; onNoDecisionit does nothing — a swarm's committed disposition is the status quo: no admission, no removal, no amendment.
A spec with a phase whose coordination is collective SHOULD hold at least ADVANCE and AMEND through such an adapter, and the charter's decisionPoints member SHOULD name the deciding role and threshold of each point. The registry itself contains no decision logic. A member's suspension or removal never alters a decision already under way: the roster that was snapshotted votes it to its end, and what a decided proposal may still do is what the registry allows at execution. The reference adapter is provided with the assets.
14.2 Agent identity profile
A member MAY be identified beyond its address by an ERC-8004 agent. An eligibility module in this profile holds an identity registry address; an account declares its agent to the module (declareAgent(agentId)), which MUST verify at declaration that ownerOf(agentId) == account or getAgentWallet(agentId) == account; isEligible then returns true iff that predicate still holds at the time of the call, and MAY be composed with modules that require reputation or validation thresholds read from the companion registries. Because the predicate is re-read live, a transfer of the agent or a change of its wallet is caught by challenge without the swarm registry tracking agents. A swarm that wants every member to be a registered agent binds this module to every role; the kernel itself never asks what an account is. After conclusion, the swarm account or the acceptance authority MAY give feedback on members' agents through the reputation registry with tag1 = "sai" and tag2 = <swarmId as hex>; this ERC does not define a feedback scale.
14.3 Obligation binding profile
A swarm MAY be bound, through objective.obligation, to an external duty such as a token-bound task tender — a non-fungible token whose vault escrows a bounty paid per accepted completion and whose acceptance authority accepts or rejects submissions. In this profile the swarm account is the fulfiller of record on the tender, and an obligation adapter is the swarm's acceptance authority: it MUST derive everything from the tender and the swarm record, never from the caller, and it concludes the swarm ACHIEVED with the submission's result hash when a submission by the swarm account is recorded as accepted, ABANDONED when the tender is cancelled, and FAILED when the tender's settlement deadline has passed without acceptance. The swarm's internal view that the work is done is an organizational fact; the tender's acceptance is a separate fact under the tender's own authority, and the swarm concludes from the latter. The reverse direction — a tender whose fulfillment descriptor names a swarm specification it will accept fulfillment from — is a convention of the tender's package format and is informative here; so is a swarm bound to several obligations, which this version expresses through child swarms, one obligation each.
14.4 Discovery (informative)
A registry instance and the swarms recruiting in it can be published for discovery as records of a value registry such as ERC-8444; a recruiting swarm is, in posture, a demand for members. This ERC defines no kind for that registry and claims nothing about it.
15. Clock
A swarm registry MUST expose ERC-6372 clock() and CLOCK_MODE(). One registry instance uses one clock for every swarm it holds; durations in a spec are seconds of that clock and MUST NOT be converted between clocks to claim that a window will be met. The reference implementation uses mode=timestamp.
16. Interface identifiers
| interface | ERC-165 id |
|---|---|
ISwarmRegistry | 0x085203a4 |
IEligibility | 0xd0ede3d8 |
ISwarmAccount | 0x25e98353 |
IResourceAdapter | 0x8e20b298 |
17. Reserved extension points
The following are reserved: named so that implementations leave room for them, but not specified, and no conformance may be claimed for them under this document.
- Allocation functions: on-chain weight and share computation from the ledger and the contribution stream; this ERC declares the payoff posture and leaves the formula to the charter and the swarm account.
- Internal market primitives: bidding and pricing of sub-tasks among members.
- Bonds and penalties: member stakes, slashing on provable procedural violations, consequences of leaving.
- Joint-action binding: a normative relation between a swarm's roster and the participant set of a coordinated-act primitive such as ERC-8001.
- Matchmaking: emergent formation by a contract that merges independently registered intents into one swarm.
- Multiple obligations on one swarm; splitting and merging of swarms.
- Privacy: zero-knowledge eligibility, anonymous members, hidden contributions.
- Cross-chain members and obligations; per-phase timers beyond the input window; delegated seats; per-member permission suspension finer than the seat.
18. Errors
Implementations SHOULD revert with the following custom errors (or equivalent reason strings) so that relying contracts and tests can distinguish causes. Specification: SAI_RolesOutOfRange, SAI_ZeroRole, SAI_DuplicateRole, SAI_RoleBounds, SAI_ModuleNotContract, SAI_PhasesOutOfRange, SAI_Phase0MustRecruit, SAI_UnknownTerm, SAI_AuthoritiesLength, SAI_AuthorityShape, SAI_UnknownRoleAuthority, SAI_InitiatorRoleAuthority, SAI_InitiatorRoleMin, SAI_MinMembers, SAI_MaxMembers, SAI_ActivationAuthority, SAI_RecruitmentWindow, SAI_NoLivenessFallback, SAI_NoConclusionPath, SAI_AdvanceAuthority, SAI_RecruitAdmitter, SAI_DurationTooLong. Objective: SAI_ZeroObjective, SAI_MilestoneCount, SAI_MilestoneIndex, SAI_MilestoneStatus, SAI_MilestoneFinal. Lifecycle: SAI_UnknownSwarm, SAI_WrongState, SAI_NotAuthorized, SAI_RosterNotMet, SAI_RosterMet, SAI_RosterCheckDeferred, SAI_WindowOpen, SAI_BeforeDeadline, SAI_NotInactive, SAI_LastPhase, SAI_CarriedShape, SAI_NotCarriedMember, SAI_BadOutcome, SAI_MilestonesIncomplete, SAI_ParentUnavailable. Charter: SAI_StaleCharter, SAI_SameCharter, SAI_SignatureExpired, SAI_BadSignature. Membership: SAI_BoundaryClosed, SAI_Gated, SAI_InputClosed, SAI_UnknownRole, SAI_InitiatorRoleSeated, SAI_RoleFull, SAI_SwarmFull, SAI_SeatTaken, SAI_RoleMismatch, SAI_Ineligible, SAI_NotMember, SAI_NoSeat, SAI_NotCandidate, SAI_NotSuspended, SAI_NoModule, SAI_GraceRunning, SAI_Unevaluable, SAI_SuccessorHasSeat, SAI_ContributionsDisabled. Resources and vocabulary: SAI_ZeroKind, SAI_ZeroDigest, SAI_AnchorMissing, SAI_DigestMismatch, SAI_UnknownResource, SAI_Detached, SAI_UnknownDimension, SAI_ZeroTerm, SAI_TermExists. Authority and account: SAI_NotHolder, SAI_ZeroAuthority, SAI_NotTransferable, SAI_AccountSet, SAI_AccountMismatch.
Rationale
Why a hashed spec and a hashed charter, not one or the other. What the registry executes — who may admit, how many seats, when a fallback opens — must be the very object whose hash identifies the swarm, or a candidate could accept a charter and find a different rule running. What nobody on-chain executes — the goal in words, the allocation formula, the orchestration — gains nothing from being a struct and would cost every swarm the gas of storing prose. The two layers each commit, and the pack names the spec it describes, so a reader verifies both with two hashes.
Why admission is two-sided. An authority that could place an account in a swarm without its consent could bind an agent to a charter it never saw, and an agent that could seat itself in a gated swarm would make the boundary meaningless. Requiring both acts, in either order, gives every member a recorded acceptance of a specific charter version, which is also what makes amendment with an exit window coherent. An initiator who lists a well-known agent, invites it, or has received a contribution from it has not thereby made it a member.
Why leaving is always available. An autonomous account cannot be made to work; a rule forbidding exit would be a rule the registry cannot enforce. Recording the exit, flagging it when it falls in an amendment window, and leaving consequences to bonds and reputation profiles keeps the ledger honest about what it can and cannot do.
Why eligibility is a module and challenge is permissionless. Trust about agents is expressed in other registries — assertions, identity states, reputation — under rules that change with the schemes. Copying any of it into the swarm registry would create a second, stale source. Reading it live through a module whose address is in the hashed spec, and letting anyone trigger the re-read, is what makes a revoked assertion or a retired identity move the boundary without a judgment call; and treating a reverting module as "unevaluable, no change" rather than "ineligible" keeps a broken or griefing module from emptying a swarm. "Unknown" stays unknown: no proof received, a verification service unreachable, a heartbeat missed are never read as success, failure or malice by the kernel.
Why time is a fallback and never a success criterion. A swarm exists for a purpose, and "the deadline passed" says nothing about whether the purpose was met; so no transition records ACHIEVED on a clock. But a swarm that nobody can close holds escrow and seats for ever, so every state has a permissionless exit after a declared duration and a spec with no such exit is invalid — including the forming state, where an account can already be bound and funded. The outcome a fallback records is ABANDONED with a reason that names the clock, never a verdict. Time keeps its proper jobs: recruitment validity, response windows, grace periods, settlement windows, stagnation.
Why posture is a coordinate, and why its declared dimensions are a vocabulary. A closed list of modes fails twice: it is incomplete the first time someone needs "redundant work, shared reward" or "pipeline, winner paid", and it is not independent because "tender" is already "competition then cooperation". Five dimensions that each answer one question compose into every named mode and into modes without names; the dimensions are not claimed to be orthogonal in a mathematical sense — some combinations are odd — only to be separately configurable and separately readable. Two of the five are executed (boundary, admission) and three are declared (payoff, topology, coordination); declaring them on-chain lets a counterparty condition on posture — a tender that accepts only shared-payoff swarms as fulfillers, an indexer that lists open bidding phases — without parsing a document. Making the declared values registered terms rather than enum members means a new way of working — a tournament tree, a relay, a bounty board — becomes a registered word with a published description, usable by every swarm on the instance, without a new kernel.
Why the commitment is a separate hash and the registry decides what is material. Asking every member to re-sign every re-ordering of sub-tasks would make charters unamendable in practice; letting any change pass silently would make acceptance meaningless. The split follows what a change does to the members' commitment — but a split the amending authority alone classifies protects nobody from that authority: the exit window would guard members against the very party that decides whether it opens. The registry cannot read charter text, so the pack commits the economic terms — allocation, costs, liabilities, data access, actions in the members' name — as one member with its own hash, next to the objective's, and a change of either hash is material by construction. The declaration survives only as a one-way override. What the registry protects is then a structural fact anyone verifies with one hash, not a judgment it would have to trust.
Why challenge guards gas and expel asks again. A module answers a question about the world, and the registry attaches consequences to the answer; the registry must therefore be sure that what arrived was an answer. A revert is plainly not one. A false produced because the caller left the module too little gas to read its sources is not one either, and the module cannot tell the two apart from inside, since the starvation is its caller's choice; the registry can, from the gas left after the call, with one rule for every module. By the same logic the grace period is a question, not a verdict: the clock says only that enough time has passed to ask again, and whoever asks gets the current answer, which may be that the member is back. Making both entry points re-adjudicate on evidence keeps suspension, expulsion and reinstatement on one footing and leaves the REINSTATE authority its proper role, reinstatement on judgment.
Why nobody is authorized after dissolution. Authority kinds answer who may act for a living swarm; after dissolution there is no swarm to act for, and an ADDRESS holder keeping power that a role holder loses would make the fate of a swarm's assets depend on an incidental choice of authority kind. Ending every authority with the swarm gives the account one question to answer — who may recover what is left — and the profile requires it to answer on-chain, so that the answer is a term of the charter and not a side effect.
Why decisions are an adapter and not kernel logic. A swarm registry that voted would have to re-specify policies, bodies, rounds and finality. Those exist, with their own record and their own finality, in ERC-8436; a swarm is one of the relying contracts that standard was written for. Holding a decision point through an adapter keeps the kernel's trust footprint to its ledger and lets each swarm choose how much of its steering is collective, by policy and by decision point, without a kernel default. Because that standard freezes a policy's roster, a swarm with changing membership meets it through snapshots: the deciding role at the moment of filing becomes the roster of one policy version, and later changes to the ledger reach later decisions only. The proposal is the exact call and is part of the issue's subject together with the charter version and the phase, so what was decided and what is executed are one object, and a call that reads advance is a different transition in every phase.
Why the kernel holds no value. Payment belongs to the account that received it and the obligation that released it. The registry's contribution is the attribution those need — who was a member, in what role, in good standing, under which charter version, in which phase, with which recorded contributions — kept apart from the money so that a dispute over allocation never has to reinterpret the record of membership, and so that work done by one roster is not paid to another.
Why a revision counter. Events already order changes within a chain's history, but a relying contract or an off-chain reader asked "was this change made on top of that state?" needs a number the registry itself guarantees to be strictly increasing per swarm. The counter is that number; emitting it with the selector that produced it makes the event stream an organizational change log without a second event per fact.
Why origin, initiator and authority are three records. A bot that submits a formation for ten agents is an executor, not a founder; a task that triggers a recruitment has no permanent owner; a decision that spawns a swarm is a procedure, not a person. Recording what the formation came from, who submitted it and who holds power separately keeps credit and power apart: an initiator's trust assertions and history help a candidate decide whether to join, and give the initiator nothing a spec did not grant.
Why the name. "Swarm" names a group formed around a purpose whose members act autonomously but in concert, the sense the term has in collective intelligence; the proposal is unrelated to decentralized storage networks that share the word.
Relation to the coordinated-act primitive. ERC-8001 answers "did these specific accounts agree to this specific action"; this ERC answers "what is the group, who is in it now, why does it exist and when does it end". A swarm may use the former for a joint action among its current members, and the former's participant list may be a swarm's roster snapshot; the binding is reserved (Section 17) until a deployment needs it normatively.
Backwards Compatibility
This ERC introduces new contracts and requires no change to existing standards. Agent identity registries, trust-assertion registries, collective-decision registries and task tenders are read through adapters and modules; nothing here writes to them or reinterprets their records. ERC-165 interface detection, EIP-712 typed data, ERC-1271 contract signatures and ERC-6372 clock disclosure are used as specified in those documents.
Test Cases
Deterministic vectors are provided under the assets of this proposal. Each file states how its values were produced and is self-describing:
spec-hash.json: two completeSwarmSpecvalues (a minimal single-phase gated swarm; a two-phase tender) with their ABI encodings andspecHash, produced by an independent ABI coder and re-derived in Solidity;join-digest.json: EIP-712Joindigests for a fixed chain id and verifying contract, across seat epochs and phases;charter-hash.json: the two example packs with their canonical lengths,charterHash,objectiveHash,commitmentHashand per-phase descriptor digests, produced by two independent canonicalizers;term-ids.json: the dimension identifiers, every base term, the reserved role, origin and resource-kind identifiers, the decision-point questions and the effect identifiers;interface-ids.json: the ERC-165 identifiers of Section 16.
As informative context, the reference implementation has been exercised by a test suite covering: every validation rule of Section 3.2 and Section 8; the lifecycle state machine including every fallback, the deferral of the roster check after an advance, the two-stage inactivity path, conclusion rules for milestoned objectives, the restriction of CONCLUDING to leave, and succession with carried candidates; two-sided admission in both orders, gated and open phases, caps, eligibility at completion, challenge with a true, false and reverting module, a gas-starved module that catches its own reads, reinstatement on evidence through challenge and through a re-evaluating expel, expulsion after grace, reinstatement on judgment with re-evaluation, removal of candidates versus members, substitution in a closed phase, and signed acceptance by an externally owned and by a contract account including replay after an exit and after a phase change; advances under all three admission rules with lazy exits and counter resets; contribution modes, milestones, amendments made material by a changed objective or commitment hash and the one-way flag, the exit window and the completion hold, authority transfer, no authority of any kind after dissolution, account binding, representation and the account's recovery path, resources with a verifying adapter, and vocabulary registration; the decision adapter against the ERC-8436 reference registries (roster completeness, policy versions per decision, the obligation rule, Yes, No and NoDecision results, a material amendment invalidating a pending proposal, a proposal approved in one phase refused in the next, two approved advances that do not chain across phases, a failed enactment that stands and is retried); and two end-to-end cases against the ERC-8436 registries and a real token-bound task tender kernel — a tender-bound cooperative swarm paid through the swarm account, and a two-phase tender with a challenge and a substitution. This document does not depend on that suite; the vectors above are the normative test material.
Reference Implementation
A reference implementation is provided under the assets of this proposal:
- Types, errors and interfaces:
SAITypes.sol,SAIErrors.sol,ISwarmRegistry.sol,IEligibility.sol,ISwarmAccount.sol,IResourceAdapter.sol. - Registry:
SwarmRegistry.sol— non-upgradeable, permissionless — with its logic in the external librariesLifecycleLib.sol,MembershipLib.solandResourceLib.sol, the validatorSpecValidator.sol, the storage layoutSAIStore.soland the signature helperSignatureChecker.sol. - Account:
SwarmAccount.sol, acting for the swarm under theREPRESENTauthority. - Adapters:
SwarmDecisionAdapter.sol(Section 14.1) against the vendored ERC-8436 interfaces, andSwarmTenderAdapter.sol(Section 14.3) against the kernel interfaceITaskTenderKernel.solit calls (informative). - Eligibility modules:
AllowlistEligibility.sol,CompositeEligibility.sol,ERC8004Eligibility.sol(Section 14.2),AssertionLevelEligibility.solandIdentityStateEligibility.solover the informative interfaces of a trust-assertion registry and an address-anchored identity registry. - Schemas and examples:
swarm-pack.schema.json,swarm-spec.schema.json,swarm-pack-cooperative-tender.jsonandswarm-pack-two-phase-tender.json.
Under solc 0.8.24 with the IR pipeline the registry compiles to 16,480 bytes of runtime code and the three logic libraries to 20,903, 12,047 and 4,819 bytes, each within the EIP-170 limit; the libraries are linked at deployment and execute in the registry's context.
Security Considerations
Sybil rosters. This ERC does not establish that members are independent; an operator controlling N addresses holds N seats. Eligibility modules are where identity, controller binding and per-operator caps belong, and charter authors are advised to disclose what their rosters assume.
Initiator power. The initiator holds no power by default. A spec that assigns every decision point to INITIATOR is valid and should be read as what it is: a swarm run by one account. Candidates read the authorities before accepting the charter; indexers are advised to surface them.
Modules that can be made to answer true. The mirror image of a griefed false is a bought true: a member suspended because its balance fell under a threshold borrows the amount in a flash loan, challenges itself back to ACTIVE and repays within the same transaction, and the same borrowed balance passes admission and the eligibility check of a SELECT advance. Nothing in the registry distinguishes a borrowed balance from an owned one, and the evidence paths of Section 6.4 make an answer effective at once. The defence is in the module's inputs, which is why Section 6.4 forbids answering from a quantity the evaluated account can change within the transaction: checkpointed, time-weighted or held-since values instead of live balances. Charter authors are advised to bind no module whose true can be rented for one transaction, and to prefer identity and assertion registries, where a true is a record that someone else issued.
Griefing through fallbacks and challenges. Every fallback is permissionless by design and changes state only when its condition holds; a challenge changes state only when a module answers with gas to spare. A module that can be made to answer false for honest members by its inputs is a weakness of that module, not of the registry; a module that could be made to answer false by starving it of gas is defended by the registry's remainder rule (Section 6.4), because gas is the caller's choice and no module can refuse it. Module authors are advised to keep the predicate a pure read of the registries they trust, to forward gas rather than fix stipends, and charter authors to read a module before binding it. A challenger who wants an answer supplies ample gas; one who starves the call gets no answer and changes nothing.
Unbounded rosters. maxMembers may be zero. Transitions that affect a whole roster — abort, dissolve, a selective or recruiting advance — are specified so that an implementation can record the roster-wide exits lazily rather than loop over every seat; an implementation that loops anyway can be made un-dissolvable by a large enough roster, which is why the lazy form is the one the reference implementation takes.
Replayed acceptances. A signed acceptance binds the chain, the registry, the swarm, the role, the charter version, the account's seat epoch and the phase, so a signature relayed after the seat it opened has exited, after a phase change, or after an amendment is invalid; it carries no further nonce because an account holds one seat per swarm at a time.
Recruitment spam and seat squatting. Open boundaries admit anyone a module accepts; maxMembers, role maxima and the input window bound the damage, and bonds are a reserved extension. A candidate who applies and never accepts holds no seat.
Frozen rules and transferred slots. The spec is the hash of what runs; the only mutable executed parameter is the holder of an ADDRESS authority, changed by that holder alone with an event. A holder that transfers a slot to a contract it controls has changed nothing about who holds the power. Vocabulary terms are write-once, so a posture never changes meaning under a running swarm.
Compromised members. Three paths exist: a module that reflects the compromise (revoked assertion, stale identity, moved wallet) suspends on challenge; the REMOVE authority suspends or removes on judgment; the ADMIT authority substitutes a successor. Contributions recorded while suspended are flagged, not erased; allocation profiles decide what they count. A suspension takes effect for the registry's own gates at once — the member's role authority, its standing in the roster — while what the member's software keeps doing or remembering off-chain is a matter for execution environments and access systems. The path back is symmetric: an identity that lapses during a renewal and returns is reinstated by anyone's challenge, and an expel called after the grace period asks the module again before acting, so a member is never expelled on the clock alone. A suspension can still cascade: one seat below a role's min lets anyone call enforceRoster, which pauses the swarm with ROSTER_LOST; resume is then permissionless once the roster holds again, but contributions stop and the inactivity clock keeps running in the meantime, which is why the evidence-based paths above are kept short.
Decisions under way. A proposal filed through a decision adapter is decided by the roster snapshotted at filing; a member suspended or removed afterwards still votes it to its end, and the registry's refusal at execution — not a change to the proceeding — is what stops an enactment the ledger no longer allows. Two changes the registry does not refuse on are therefore the adapter's to refuse on: a material amendment and an advance. A decision approved in one phase is performed in no other, whatever the decision point, because advance is relative — two ADVANCE proposals approved in phase 0 would otherwise move a swarm through phase 1 and into phase 2 on one phase's votes — and because a selective advance replaces the deciding roster, so that members not carried would otherwise decide for those who were. Charter authors who want further security events to halt a proceeding bind that rule into their adapter's execution conditions in advance.
Allocation drift. Paying by the roster at settlement rather than by the roster, phase and charter version under which work was recorded lets later membership changes rewrite earlier entitlements. The contribution stream carries phase and charter version so that accounts can pay by the record; account implementations are advised to do so and to say so.
Settlement after dissolution. The registry holds no funds and authorizes nobody in a dissolved swarm, so representation ends with the swarm for every kind of REPRESENT authority. What happens to value in a swarm account afterwards is the account's post-dissolution rule, which the account profile requires it to state through recoveryAddress(); counterparties are advised to read it before funding a swarm, and representatives to set it before dissolution, since a zero recovery address keeps what is left where it is. A charter that wants distribution by representation settles in CONCLUDING, where the settlementWindow guarantees the time; dissolution itself stays permissionless after that window, so that settlement can never hold a swarm open.
Amendment classification. The registry protects economic terms only where the pack schema puts them: in the commitment member, hashed on-chain. A charter that states allocation or liabilities in its prose instead has moved them out of the structural rule, and a reader comparing the pack with its schema sees that. Allocation profiles are advised to read commitmentVersion and commitmentHash rather than charter text when deciding which terms a contribution was recorded under.
Timing. Windows are arrival windows; a contribution or acceptance counts only if the registry receives it in time. One instance, one clock.
Untrusted inputs. Charter URIs, contribution URIs, resource URIs and term descriptors are untrusted data. Agent members are advised to treat their contents as material to evaluate, never as instructions.
Registry instance trust. A relying contract binds to a registry instance; its deployment, upgradeability (none, for the reference implementation), its resource adapters and the libraries it was linked with are that contract's trust decisions, best disclosed to its users.
Copyright
Copyright and related rights waived via CC0.
