--- eip: 8447 title: Swarm AI Framework description: Registry and charter model through which agents form, bound, run and dissolve purpose-bound swarms with declared collaboration structure author: Gary Yang (@garyyang-finchip) discussions-to: https://ethereum-magicians.org/t/erc-8447-swarm-ai-framework/29998 status: Draft type: Standards Track category: ERC created: 2026-10-18 requires: 165, 712, 1271, 6372, 8004, 8436 --- ## 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: 1. 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))`; 2. 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; 3. 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; 4. 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; 5. **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; 6. an `ISwarmRegistry` interface 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](./eip-8436.md), member identities through [ERC-8004](./eip-8004.md), 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](./eip-8001.md) 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](./eip-8063.md) 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](./eip-8436.md) 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](./eip-8004.md) gives each agent an identity and a place for trust signals, not a group. [ERC-8033](./eip-8033.md) fixes one specific group shape — information agents and a judge — for one kind of query. [ERC-8183](./eip-8183.md) 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 `ACHIEVED` from 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 `ACHIEVED` when 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### 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`, `SUSPENDED` or `EXITED`. "Active member" means status `ACTIVE`; 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`, `ABANDONED` or `SUPERSEDED`, 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](./eip-165.md) 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 ```solidity 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 role `max == 0 || min <= max`; every non-zero `eligibility` address has code. - `1 <= phases.length <= 32`; `phases[0].admission == RECRUIT`; every phase's `payoff`, `topology` and `coordination` is a registered term of its dimension (Section 7.1). - `authorities.length == 16`. An `ADDRESS` authority has a non-zero `target` and zero `roleId`; a `ROLE` authority has zero `target` and a `roleId` present in `roles` other than the initiator role; `NONE`, `ANY_MEMBER` and `INITIATOR` authorities have zero `target` and zero `roleId`. - `minMembers >= 1` and `maxMembers == 0 || maxMembers >= minMembers`. - If `activation != ROSTER_MET` then `authorities[ACTIVATE].kind != NONE`. - The forming state has a permissionless exit: `recruitmentWindow > 0` (an `ABORT` authority only adds an earlier one). - The swarm has a liveness fallback: `maxDuration > 0 || inactivityTimeout > 0`. - A conclusion is reachable: `authorities[CONCLUDE].kind != NONE` or the objective's `acceptanceAuthority` is non-zero (Section 8). - If `phases.length > 1` then `authorities[ADVANCE].kind != NONE`. - If a phase after the first has `admission == RECRUIT` and `boundary == GATED`, then `authorities[ADMIT].kind` is `ADDRESS` or `INITIATOR` (a `ROLE` or `ANY_MEMBER` admitter 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.")` with `` 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.")` with `` 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`](../assets/eip-8447/schemas/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](https://www.rfc-editor.org/rfc/rfc8785) (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](./eip-1271.md) 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](./eip-712.md) 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](./eip-1271.md). 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 ```solidity 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 ```solidity 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 `ADMIT` authority called `admit(swarmId, account, roleId)`; or, in a phase whose boundary is `OPEN`, for a role with `selfJoin == true`, the account's own `join` counts as the authority side; - the **member side**: the account called `join`, `applyToJoin` or `acceptCharter` naming the current `charterHash`, or a valid `joinBySig` was 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 any `CANDIDATE`, `ACTIVE` or `SUSPENDED` account in a swarm that is not `DISSOLVED`, in every phase and state, and MUST set `EXITED(LEFT)`. If the call falls within the exit window of a material amendment the member has not accepted (Section 9.1), `MemberExited` MUST carry `onAmendment = true`. Consequences of leaving are outside this ERC. - `suspendMember` and `remove` MUST be accepted only from the `REMOVE` authority for an `ACTIVE` or `SUSPENDED` member, setting `SUSPENDED(AUTHORITY)` or `EXITED(REMOVED)`; `remove` of a `CANDIDATE` MUST be accepted only from the `ADMIT` authority (dropping a candidate is the counterpart of admitting one). - `reinstate` MUST be accepted only from the `REINSTATE` authority, for a `SUSPENDED` member. If the cause was `INELIGIBLE`, reinstatement MUST re-evaluate the role's eligibility module and MUST revert unless it returns `true`. This is reinstatement on judgment; a member suspended for ineligibility is also reinstated on evidence, by anyone's `challenge` or `expel`, once the module answers `true` again (Section 6.4). - `substitute(swarmId, previous, next)` MUST be accepted only from the `ADMIT` authority, for an `ACTIVE` or `SUSPENDED` `previous` and a `next` that holds no live seat and passes the role's eligibility. It MUST set `previous` to `EXITED(REPLACED)` and open a seat for `next` in the same role with the authority side done and `substituted` set; `next` becomes `ACTIVE` once 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 a `CLOSED` phase — and only the eligibility check applies. A substitution does not transfer contributions: attribution stays with the account that recorded them. - `suspendMember`, `remove`, `reinstate`, `substitute`, `challenge` and `expel` MUST be accepted only while the swarm is `FORMING`, `ACTIVE` or `SUSPENDED` (`challenge` and `expel` additionally require a role with a module, Section 6.4); `leave` is accepted in every state but `DISSOLVED`. #### 6.4 Eligibility and challenge (normative-optional) ```solidity 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](./eip-5805.md) `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.")`. The base terms, `keccak256("sai..")` 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`: `carried` and `roles` MUST be empty; every `ACTIVE` and `SUSPENDED` member keeps its seat, role and status. - `SELECT`: `carried` lists `ACTIVE` members, pairwise distinct; `roles` is empty or of equal length and names, per carried member, the role it holds in the new phase (its eligibility module, if any, MUST return `true`; the resulting roster MUST respect every role's `max` and `maxMembers`). Listed members keep `ACTIVE` with `carriedPhase` updated and `MemberCarried` emitted for each, with its role in the new phase; every other `ACTIVE`, `SUSPENDED` or `CANDIDATE` account is exited with reason `NOT_CARRIED`. - `RECRUIT`: `carried` and `roles` MUST be empty; every account is exited with reason `NOT_CARRIED` and 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 ```solidity 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 `commitment` or `objective` member by construction (Section 4.1) — or is declared material by the authority (`material == true`) although neither hash changed. It also increments `commitmentVersion` and opens an exit window of `amendmentExitWindow` seconds (`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) ```solidity 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.")`; 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 ```solidity 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`](../assets/eip-8447/contracts/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) ```solidity 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) ```solidity 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](./eip-8436.md) 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`, `remove` of a candidate; for `REMOVE`: `remove`, `suspendMember`; for `AMEND`: `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))}`, question `keccak256("sai.dp.")` for the decision point (`activate`, `abort`, `suspend`, `resume`, `advance`, `conclude`, `dissolve`, `succeed`, `admit`, `remove`, `reinstate`, `amend`, `attach`, `detach`, `set-account`, `represent`), effects `keccak256("sai.enact")` and `keccak256("sai.refuse")`, disposition `keccak256("sai.status-quo")` and `obligationId = 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; - `consumerDeadline` is 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 `Final` result with outcome type `Decided` and `outcomeYes == true` it 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; `advance` in particular is relative, so a Yes obtained in an earlier phase would perform a transition nobody voted on — and records the enactment (`Enacted`, or `Failed` when the registry refused — the decision stands and may be retried); on `Decided` with `outcomeYes == false` it records that nothing is to be done; on `NoDecision` it 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](./eip-8004.md) 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 = `; 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](./eip-8444.md); 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](./eip-6372.md) `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](./eip-8001.md). - *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`](../assets/eip-8447/vectors/spec-hash.json): two complete `SwarmSpec` values (a minimal single-phase gated swarm; a two-phase tender) with their ABI encodings and `specHash`, produced by an independent ABI coder and re-derived in Solidity; - [`join-digest.json`](../assets/eip-8447/vectors/join-digest.json): EIP-712 `Join` digests for a fixed chain id and verifying contract, across seat epochs and phases; - [`charter-hash.json`](../assets/eip-8447/vectors/charter-hash.json): the two example packs with their canonical lengths, `charterHash`, `objectiveHash`, `commitmentHash` and per-phase descriptor digests, produced by two independent canonicalizers; - [`term-ids.json`](../assets/eip-8447/vectors/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`](../assets/eip-8447/vectors/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`](../assets/eip-8447/contracts/SAITypes.sol), [`SAIErrors.sol`](../assets/eip-8447/contracts/SAIErrors.sol), [`ISwarmRegistry.sol`](../assets/eip-8447/contracts/interfaces/ISwarmRegistry.sol), [`IEligibility.sol`](../assets/eip-8447/contracts/interfaces/IEligibility.sol), [`ISwarmAccount.sol`](../assets/eip-8447/contracts/interfaces/ISwarmAccount.sol), [`IResourceAdapter.sol`](../assets/eip-8447/contracts/interfaces/IResourceAdapter.sol). - Registry: [`SwarmRegistry.sol`](../assets/eip-8447/contracts/SwarmRegistry.sol) — non-upgradeable, permissionless — with its logic in the external libraries [`LifecycleLib.sol`](../assets/eip-8447/contracts/libraries/LifecycleLib.sol), [`MembershipLib.sol`](../assets/eip-8447/contracts/libraries/MembershipLib.sol) and [`ResourceLib.sol`](../assets/eip-8447/contracts/libraries/ResourceLib.sol), the validator [`SpecValidator.sol`](../assets/eip-8447/contracts/libraries/SpecValidator.sol), the storage layout [`SAIStore.sol`](../assets/eip-8447/contracts/libraries/SAIStore.sol) and the signature helper [`SignatureChecker.sol`](../assets/eip-8447/contracts/libraries/SignatureChecker.sol). - Account: [`SwarmAccount.sol`](../assets/eip-8447/contracts/account/SwarmAccount.sol), acting for the swarm under the `REPRESENT` authority. - Adapters: [`SwarmDecisionAdapter.sol`](../assets/eip-8447/contracts/adapters/SwarmDecisionAdapter.sol) (Section 14.1) against the vendored ERC-8436 interfaces, and [`SwarmTenderAdapter.sol`](../assets/eip-8447/contracts/adapters/SwarmTenderAdapter.sol) (Section 14.3) against the kernel interface [`ITaskTenderKernel.sol`](../assets/eip-8447/contracts/adapters/interfaces/ITaskTenderKernel.sol) it calls (informative). - Eligibility modules: [`AllowlistEligibility.sol`](../assets/eip-8447/contracts/modules/AllowlistEligibility.sol), [`CompositeEligibility.sol`](../assets/eip-8447/contracts/modules/CompositeEligibility.sol), [`ERC8004Eligibility.sol`](../assets/eip-8447/contracts/modules/ERC8004Eligibility.sol) (Section 14.2), [`AssertionLevelEligibility.sol`](../assets/eip-8447/contracts/modules/AssertionLevelEligibility.sol) and [`IdentityStateEligibility.sol`](../assets/eip-8447/contracts/modules/IdentityStateEligibility.sol) over the informative interfaces of a trust-assertion registry and an address-anchored identity registry. - Schemas and examples: [`swarm-pack.schema.json`](../assets/eip-8447/schemas/swarm-pack.schema.json), [`swarm-spec.schema.json`](../assets/eip-8447/schemas/swarm-spec.schema.json), [`swarm-pack-cooperative-tender.json`](../assets/eip-8447/examples/swarm-pack-cooperative-tender.json) and [`swarm-pack-two-phase-tender.json`](../assets/eip-8447/examples/swarm-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](./eip-170.md) 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](../LICENSE.md).