Repository navigation
[PROPOSAL] RFP-005 — Symeros #225
Description
RFP ID
RFP-005 — Multisig and Coordination
Your Project Name
Symeros
Team or Organization Name
VacuumLabs
Primary Contact
Team Members
| # | Name / pseudonym | Social links | Role on Symeros | Status |
|---|---|---|---|---|
| 1 | Boris Hristov | GitHub · CV | Tech lead | Full-time |
| 2 | Ladislav Dubravsky | GitHub · CV | Senior engineer | Full-time |
| 3 | Vladislav Dunaev | GitHub · CV | Senior engineer | Full-time |
| 4 | Marek Roštár | GitHub · CV | Project manager | Part-time |
| 5 | Uroš Kočišević | GitHub · CV | Product owner | Part-time |
| 6 | cr1st0f | GitHub · CV | Advisor | Part-time |
Project Summary
Symeros is an M-of-N multisig for the Logos Execution Zone (LEZ), private by default, with an end-to-end-encrypted room for each multisig on the Logos chat module. Its configuration, members, vaults and activity are private accounts owned by the Symeros program. Funds leave a vault only when the program, inside the proof, has verified approvals from current members whose weight reaches the threshold M, or a spending limit's smaller sub-threshold k (F.8). Approvals are signatures collected in the room, with a private on-chain path when the room is unavailable. An on-chain observer sees opaque commitments, nullifiers, note sizes and validity windows, and the action's effect on any public account it touches, but not the members, the threshold, the approvals or the payload.
This matters for Logos because every production multisig publishes who controls a treasury and who approved each payment; the Bybit and WazirX losses targeted visible Safe setups at the signing layer. Symeros hides the signer set, lets a treasury disclose per category (configuration, holdings, activity) to whom it chooses, and has members decode every action from the bytes they sign, against a registry that ties decoders to bytecode.
We designed against LEZ v0.3.1 as it is: per-program shards, the plan/apply split and its private-PDA rules. No 0.4 specification exists, and rfp PR 214 would make testnet 0.4 the target, so every security rule sits in a plain Rust library with no LEZ dependency, the LEZ adapter is thin, and the 0.4 port is M5 by default, earlier if 0.4 is ready, with written assumptions. The design answers the RFP's open questions: R.2 with a compromised member (removal moves the multisig to fresh keys and voids the queue, so a removed member's approval never counts again), limits that cannot become arbitrary calls, and per-category disclosure. §8 lists the attacks we found on our own design, each with its defence and test. One we state plainly: on LEZ 0.3.1 a compromised executor can stall the multisig, including the removal meant to evict them, by re-consuming its accounts. They cannot move funds, and clients detect it, but only a LEZ protocol rule closes it, which we will propose to the LEZ team in M1 (§5).
Before filing, on our own time, we measured approval verification with real proving on LEZ 0.3.1's RISC Zero version: each Schnorr approval adds about 269k cycles, so eight add about 2.2M, well within the 10M public gas cap. The rest of a private execute does not depend on M, and LEZ publishes no real-proving time for it, so M1 measures a full execute end to end. If PR 949 keeps its fee window of about 200 s, private executions will need GPU proving, which M1 measures where available (§9).
Technical Approach
LEZ references are to logos-execution-zone v0.3.1, chat references to logos-chat-module v0.3.0. Requirement IDs follow the RFP (F.1 is Functionality item 1).
1. Architecture
symeros-core plain Rust, no LEZ dependency; runs in the guest and in clients:
config, members, roles, weights, policies, epochs, action encoding,
approval digest and verification, R.2 rules, time lock, limits, replay
symeros-lez03 thin LEZ v0.3.1 program adapter (plan/apply)
symeros-lez04 the equivalent adapter for testnet 0.4 (M5 or earlier)
symeros-registry registry program, same layering
symeros-client transaction building and proving, decoding, room protocol, disclosure viewer
symeros_module Logos core module exposing the client API (U.1)
symeros_cli CLI module driven through logosctl (U.3)
symeros-ui Basecamp mini-app, ui-qml-backend template (U.2)
Every rule an auditor cares about lives in symeros-core, property-tested on the host and run unchanged in the guest; the adapter only maps instructions to LEZ effects and chained calls.
The LEZ core module (lez_core 0.5.1) has no helpers for group keys or private PDAs. The v0.3.1 wallet FFI can send generic private transactions involving shared PDAs, and symeros-client builds on it where it covers our transactions (M1 checks how far). On top it adds approval-set assembly with Merkle paths, local plan and apply before proving (U.7), background proving with progress, and the disclosure viewer.
2. State on LEZ 0.3
All Symeros accounts are private PDAs of the Symeros program by default. Seeds are 32-byte hashes of the labels shown; msid is the config account's own id (below).
| Account | Seed | Holds |
|---|---|---|
| Config | H("cfg" ‖ creation_nonce) |
Hot state, laid out below; everything else is committed in policy_hash |
| Members | H("mem" ‖ msid) |
Per member: stable id, approval key with scheme tag and the epoch it was set (key_since), roles, weight, account commitment. Padded to 16 or 64 members |
| Limits | H("lim" ‖ msid) |
Per-limit counter and period total, with copies of config_epoch, the limit-policy root and the initialised and migrating flags, updated in the same transaction as the config |
| Vault | H("vault" ‖ msid ‖ asset) |
One asset each. Vault ids are committed in policy_hash, and clients check deposit addresses against them |
| Mailbox | H("box" ‖ msid ‖ member_id) |
One per member, written by that member alone: enrolment and on-chain proposals, approvals or rejections. Fixed size |
| Activity log | H("log" ‖ msid) |
Ring buffer of {seq, action hash, outcome, approver ids, validity window}, plus the contents of each live queue entry |
Config layout. A private note is padded to at least 512 bytes. The 97-byte PDA header and the account encoding (56 bytes for one shard) leave 359 bytes before a note becomes recognisable by length. The config uses 277:
policy_hash(32): hash of the member root, action-set root, limit-policy root, trusted verifiers and their threshold, M, time lock, expiry cap (the longest expiry a proposal may carry) and vault ids. The executor supplies the preimage andapplychecks it.config_epoch,queue_gen, and theinitialisedandmigratingflags (13).- Pinned ids of the members, limits and activity-log accounts (96).
- The replay window:
lowplus two bits for each of 256 slots (72). queue_root(32), and a successor commitment that is zero unless migrating (32).
Why msid is the config's own id. A private PDA's id includes keys chosen by its creator (for_private_pda in program/mod.rs), but the seed grant authorises by program and seed alone. If a seed could be reused, a group-key holder could create a counterfeit config under it, list themselves as 1-of-1, and spend the real vault. So every seed derives from msid, and plan rejects any instruction whose config id is not msid; a counterfeit config's seeds reach only its own empty vaults. The config pins its members, limits and log ids, so an executor cannot divert log entries to a twin the auditors cannot open.
Queued actions are bound to the config. A program cannot tell which seed an unauthorised input account was created under, so queue entries are not accounts. The config holds queue_root, a Merkle root over the 256 window slots; a live slot's leaf is H(msid ‖ seq ‖ H(action) ‖ eta ‖ approver commitment ‖ config_epoch ‖ queue_gen). queue writes the leaf and records its contents in the activity log, which every executor can read. execute supplies them with an eight-level path, and apply checks the leaf, checks H(action) against the action being executed, and clears the slot. An entry from another configuration, or for a different action, cannot match.
Members account and root. The config's apply sees one shard, so it checks membership against the member root with Merkle paths; the members account is the copy members recover from and auditors see. A configuration change updates both from one delta.
Group key versus program authority. The LEZ wallet shares a private account through one Group Master Secret (shared-account guide). A regular shared account is 1-of-N (every holder derives the authorisation key), so we never use one. A private PDA's holders can see and update it, but only the owning program can spend, through ChainedCall.pda_seeds, scoped to the callee subtree since PR 683. Symeros gives the group key only to execute-role members and enforces the threshold in the program. Other members get per-account viewing keys plus the full keys of their own mailbox; auditors get viewing keys only.
Replay. Each proposal has a sequence number seq. The config's window over [low, low+256) has queued and executed bits, both set inside the proof, so a number is queued at most once and executes at most once; a suspended action (§4) is re-proposed under a new number. Only a full-quorum action (approved by every approve-role member) may use a seq above the window, which advances low and voids older numbers, a power the quorum already has through void_queue. Spending limits use their own counters in the limits account. Replay protection never depends on an account a key holder could duplicate under other keys.
3. Approvals, enrolment and R.1
What a member signs. D = H("SYMEROS/APPROVE/v1" ‖ zone ‖ program ‖ msid ‖ config_epoch ‖ seq ‖ H(action) ‖ expiry), where zone is the LEZ chain identifier, program the Symeros program id and expiry the last time the approval may be used; config_epoch fixes the time lock in force. The action contains the target's program id and expected image id, the borsh instruction bytes, the account list with shard selectors, and the vault. Rejections, proposals and executor authorisations use their own domain tags.
Signature schemes. BIP-340 Schnorr over secp256k1 is the default: LEZ uses it for public-transaction witnesses, and Keycard, the smart-card wallet the LEZ wallet supports in keycard_wallet, signs it over any 32-byte digest. ECDSA P-256 is second, for platform secure enclaves that sign a raw digest; it was cheaper in our benchmark. WebAuthn passkeys, which sign authenticator data rather than D, are out of scope. Every key carries a scheme tag, so hardware signers can be added without changing the program's rules; hardware signing itself is out of scope.
Verification in apply, following sequencer_stake::verify_approvals:
- The proposer's signature and propose role, on the proposal envelope.
- Per approval: membership, the approve role, strictly increasing member ids (no member counts twice), a valid signature over
D. - Summed weight of at least M. M is a weight; with the default unit weights it is a number of members, and with custom weights fewer than M members can reach it, as soft F.1 implies.
- The current epoch, or for a queued action the R.2 recount (§4).
- The replay window, and for a queued action its leaf in
queue_root. - Expiry against the declared validity window.
- The executor's execute-role signature, bound to
seqand the validity window (§4). - Under the public posture, the target's current image id against the signed one (§7).
All three roles are thus enforced by the program on both the room path and the on-chain path.
Approval keys and enrolment. Members approve with a dedicated key, not their LEZ account, which would cost a private transaction per approval. Each key is tied to a LEZ account once, so every approval is signed for a member's LEZ account, as the RFP asks, by a key that account authorised:
- Executors give each member the full keys of their own mailbox and viewing keys for every mailbox (group-derived keys are separate per seed).
- The member submits a private transaction authorised by their own LEZ private account (
is_authorized, through the authorisation key from PR 669), writing to their mailbox an approval-key signature overH("SYMEROS/ENROL/v1" ‖ msid ‖ member_id ‖ account_commitment), whereaccount_commitment = H("SYMEROS/ACCT/v1" ‖ account_id ‖ salt)uses the account id the program sees and the salt from the join card. - An executor folds the enrolment in. The program checks the authorisation, the approval-key signature and that the commitment matches the member's entry.
Enrolment does not change config_epoch, so it never voids pending approvals; a new member or rotated key cannot approve until enrolled. The multisig becomes initialised when every founding member has enrolled, and until then every spend path aborts. Configuration changes still work before that, so the enrolled founders can drop a founder who never enrols, and clients show no deposit address until initialisation.
4. Lifecycle (F.1–F.3, F.5–F.8) and R.2
Create (F.1).
- Each invitee produces a signed join card (chat address, approval key and scheme, a key for receiving sealed keys, a commitment to their LEZ account), shared as a file, QR code or one-to-one chat.
- The creator builds the configuration: members, threshold, roles, weights, time lock, limits, posture and action set.
- The creator seals the group key to execute-role members only; every other member gets viewing keys and their own mailbox keys.
- Members enrol.
The action set is unrestricted (F.3) or an allow-list in policy_hash. An execute uses three Symeros private accounts and one chained call, leaving four of the wallet's seven padded slots and nine of LEZ's ten chained calls for the target; larger targets execute but no longer match the standard padding, a documented F.3 limitation.
Propose, approve, reject (F.2). Proposals and signatures travel in the room. On the on-chain path a member writes a proposal (body encrypted and padded), approval or rejection to their own mailbox in a private transaction, every member can read it with their mailbox viewing keys, and an executor collects it. Nothing is published in the clear, which is how we read F.2's limit on on-chain payloads, and no mailbox transaction touches the config. Proposals carry an expiry the program enforces. Rejections are signed and advisory: once rejection weight exceeds total approve weight minus M, quorum is out of reach and clients mark the proposal dead. Approvals cannot be withdrawn; to stop an approved action the quorum uses void_queue or lets it expire.
Queue and execute (F.2, F.3, F.7). With no time lock, one execute does everything: plan emits effects on the config, vault and log plus a chained call to the target with the vault's seed, and apply verifies. With a time lock:
- LEZ sets validity windows in
planand accepts open ones by default, so the window rules are Symeros' own, enforced inplan: every Symerosplandeclares a bounded timestamp window, never open and at most ten minutes wide, and copies its bounds into the effect it emits. queueverifies quorum; the config'sapplyderives the earliest execution timeetaas the window's end plus the delay and stores it in the queue entry.execute'splandeclares its window the same way; the config'sapplychecks the copied window start against the entry'seta.- We do not read the public clock account, which would publish the program id and deadline. Window checks trust the sequencer's block timestamp, a stated threat-model assumption.
Who executes. The proof needs group-derived keys, so the executor is an execute-role member's client, whose execute-role signature is verified in the proof and never published.
Configuration changes (F.5) run through the same flow as actions targeting Symeros: add or remove a member; change the threshold, roles, weights, time lock, limits, action set or verifiers; rotate an approval key. Changes are deltas verified by Merkle update paths, so apply writes exactly what signers decoded. A change names its base epoch and fails if another change executed first, so changes cannot undo each other. Every configuration change increments config_epoch; void_queue, a quorum action but not a configuration change, increments only queue_gen; enrolment changes neither. Member ids are never reused.
Removal migration. A removed member keeps whatever keys they had. So removing a member, or revoking a member's execute role, names a successor configuration:
- Its state must equal the current state minus the member (or the role), under a new epoch; clients check this before anyone signs, so a removal carries no other change.
- A non-removed executor generates the new group secret and seals it to the successor's executors.
- The action commits the successor's config and vault ids. Executing it sets the old config to
migrating, which allows only transfers to those ids, and voids every queued action. - Funds move in follow-up transactions of one or two vaults each. Enrolment records are copied over, so no member has to act.
Other changes (threshold, weights, propose and approve roles, additions, limits, action set, key rotation) do not migrate.
R.2, precisely.
- The cutoff is the queue transaction (with no time lock, the execute). An approval not yet recorded by one stops counting when any configuration change executes and must be re-signed.
- A removal during a time-lock delay voids the queued action through migration; it must be approved again under the successor, and the removed member's approval never counts.
- A queued ordinary action survives changes that neither remove a member nor revoke an execute role, recounted at execution: only recorded approvers still holding the approve role, with keys not rotated since queueing (
key_since), count at current weight against the current threshold. If short, it is suspended and must be re-proposed under a newseq. Queued configuration changes never survive another change. void_queueincrementsqueue_gen, making everything queued before it unexecutable. It only removes power, so it is exempt from the time lock, a deliberate F.7 exception recorded in the decision log.- "Submitted for execution" means the execute transaction is included; settlement is atomic.
Roles (F.6). Propose, approve and execute are independent bits; we read F.6's "proposing key" as a role, with one approval key per member. Every configuration change re-checks the invariants: at least one proposer, at least two executors (one for a single-member multisig), and approve-role weight of at least M. The program enforces all three roles (§3, and the executor signature above). Liveness: every on-chain step needs an executor's proof, so Symeros assumes one honest executor; with two, a lost or withholding one can be removed (a malicious one can stall that on 0.3.1, §5). If every executor loses the group secret, the private state is unrecoverable.
Spending limits (F.8). A policy names a token program (the native token, or an immutable program whose transfer encoding Symeros knows, initially lez-programs token; clients refuse mutable ones, because a seed grant passes to the callee's whole call tree), one vault, a per-period cap, the eligible members and a sub-threshold k.
spend_within_limitverifies k approvals overH("SYMEROS/LIMIT/v1" ‖ zone ‖ program ‖ msid ‖ limit_id ‖ vault ‖ counter ‖ recipient ‖ amount ‖ config_epoch ‖ expiry), increments the counter and updates the period total in the limits account, then builds the transfer call itself. It does not touch the config and aborts whilemigrating. This is the one path below M, as F.8 allows; R.1 governs every other.- Eligible members sign; an execute-role member's client submits, since the proof needs the keys. Eligibility does not require the execute role; with k = 1 an eligible executor spends alone, our reading of F.8's "a member".
- The period comes from the validity window, which
plankeeps bounded and at most ten minutes wide and copies into the effect; the limits account'sapplychecks it lies inside one period and never moves the counter back to an earlier period.
5. Coordination room (F.9)
Binding. A member's LEZ account and chat identity are linked through their approval key. The first half is proven on-chain at enrolment (§3). For the second, the member posts Sign_approval(H("SYMEROS/BIND/v1" ‖ msid ‖ bind_counter ‖ chat_account)) from that chat account. The chat layer authenticates the sender's account address (chat_module.lidl), and other clients check the signature against the enrolled key, so a statement replayed from another account fails. The binding lives only in the encrypted room and members' clients and never touches the program, so a re-bind never invalidates pending approvals.
Restarts. In chat module v0.3.0 every init creates a new chat account (the module says restoring such conversations "is being worked on"), and a new installation cannot post into the old group until re-invited. After a restart:
- The client sends a new statement, with a higher counter, one-to-one to any current member, whose client relays it into the room.
- Clients show the re-bind to everyone, including the member's previous chat account, and re-invite the new account after a grace period (default one hour) in which any member can object; an objection holds the re-invite until a quorum confirms it.
- A member who sees a re-bind they did not make rotates their approval key, the only key that can sign one.
When accounts persist, this goes away; a standard identity registry such as the 0.4 roadmap's would replace the statement behind the same interface.
Provisioning. The creator's client creates a group with an empty name and description and adds each member's chat account from the join cards. The group commits each add by de-MLS vote (the chat module's decentralised agreement on MLS group changes), which can take minutes, shown as pending. Members post binding statements, and the room is ready when all N verify. From then on clients compare the group's member list (list_group_members and every members_changed event) with the bound set and alert on any difference.
Protocol. Signed, typed envelopes (propose, approve, reject, execution notice, digest, bind, successor) bind the multisig, epoch and seq; unbound senders are dropped. The room carries no trust.
| Room behaviour | Effect | Handling |
|---|---|---|
| Withholding proposals or approvals | Delay | The on-chain path; clients show which members have not been seen |
| Reordering, replay, injection | None | Approvals bind epoch, expiry and seq; the window blocks re-execution; unbound senders are dropped and forged signatures fail in apply |
| Different views for different members | Different proposal sets | Approvals sign the decoded action's hash; clients exchange signed digests of their proposal sets, and a mismatch raises an alert and points to the on-chain path |
| A malicious rotation that re-adds a removed member | Exposure of future messages | Clients join a new room only when every member has a binding statement verified against an enrolled key |
Room membership vs program membership. Chat module v0.3.0 has no remove-member call (draft PR 92 adds one), so every membership change rotates the room: the executing client creates a group for the new member set, posts a signed successor notice in the old room, and clients freeze the old room as history. Invitations reach each invitee's deterministic inbox topic, as for any group conversation; an observer sees installations invited at about the same time, but nothing marks it as a multisig room; invites are staggered (Privacy 3, item 4). When removal ships, we switch to removal plus MLS re-keying and keep rotation as the fallback. Until reconciliation completes, a removed member can still see room history, messages from clients that have not processed the change, and private state through their old keys; after migration, only what they had already seen.
What a key holder can still do: freeze and bloat. On LEZ 0.3.1 anyone holding a private account's nullifier key can consume it through any program, with one private proof and no fee. A key holder, current or removed, who keeps re-consuming the config can freeze the multisig, including the removal and migration meant to evict them, though never spend. They can also attach another program's shard to a Symeros account, making its notes larger and recognisable. On 0.3.1 we bound and detect this, not block it:
- Only execute-role members hold the group secret.
- No instruction a non-executor member can submit touches the config: enrolment folding is submitted by executors, and limited spends touch the limits account.
- Clients detect a consumption that leaves Symeros' state unchanged, or a foreign shard, and name the executors (current or removed) who could have caused it.
If PR 949 lands as written, each consumption would cost the griefer a fee. Closing the hole needs one protocol rule: a private PDA may be consumed only in a transaction where its own program acts on it. We will propose it to the LEZ team in M1; until LEZ has it, the threat model records the freeze and the foreign shard as residual risks.
6. Privacy (F.10, U.5, Privacy 1–3)
Postures. Fully private is the default; fully public (F.10) makes the member list readable on-chain. Two mixed postures (soft F.3) are in scope, and our clients support no others. The log and mailboxes follow the configuration's posture; coordination content is always private. Against the RFP's ten data items:
- Public holdings, private configuration reveals the multisig's existence (its vaults are visible), vault balances and each execution's effect on them. It hides the members, threshold, approvals and attribution, pending proposals, payloads beyond their effect on the vault, and the co-signing graph.
- Private holdings, public configuration reveals the existence, member list, threshold, roles, action set, configuration history, and the activity log with approver attribution. It hides balances and payloads.
If any Symeros account in a transaction is private, the transaction follows the private-posture rules: image-id check in clients (§7), errors from local pre-execution (§11), proving time as the ceiling (§9).
What a private Symeros transaction publishes on 0.3.1 (message.rs, circuit_io.rs):
- Nullifiers and new commitments, padded to the wallet's seven private slots.
- Encrypted notes, at least 512 bytes. The config, limits account and vaults fit under that floor (§2); the members account, mailboxes and log use fixed larger sizes, so configuration changes and executions are recognisable by note length as coming from some program using those sizes, and the members account's class (16 or 64) hints at the multisig's size.
- Validity windows.
- An image claim. Symeros is deployed immutable and our client opts into the Undisclosed claim (PR 824), so the chain does not learn that Symeros ran; the anonymity set is the immutable programs, small at first, and fixes ship as a new deployment with migration. PR 824's shadow programs publish no claim but have a different id from the audited program; M3 tests them as a private-posture option.
- The target's effects on public accounts, documented per action type.
A private transaction needs no public signer or nonce, and 0.3.1 charges it no fee. Under PR 949 as it stands, the fee is a native transfer ahead of the root call, which a program-owned private account cannot authorise. So the executor would pay from an authorised account of their own, and the client warns that this may link that account to the multisig. If PR 949's final form lets a program pay the fee from its own private account, a Symeros fee vault would keep the executor unlinkable, and we would switch to it.
Privacy 1 and 2. No Symeros transaction or room message publishes data identifying a member's account or chat identity as belonging to the multisig, or their role; the exception is the fee payer under PR 949 as it stands (above; Privacy 3, item 11). Room invitations carry an MLS Welcome with no marker of the room's purpose, so they reveal only that some installations were invited at about the same time (§5; Privacy 3, item 4). The U.5 documentation carries a table per transaction type and posture covering the ten data items.
Privacy 3: correlations we document.
- Timing of room traffic against transactions; clients add a random submission delay.
- Validity windows. Only programs set windows in 0.3.1, so one shows that some time-bounded program ran. Bounds are bucketed, and an execute's window starts at a random offset after
eta, so queue and execution cannot be paired. - The image claim, for clients not using the undisclosed claim.
- The group's delivery topic, linkable to peers on the Store and Filter paths (light-client retrieval and subscription), and co-invite timing: invitations at provisioning and each rotation reach invitees' inbox topics at about the same time. Invites are staggered; chat-module member removal would end rotation invitations. We recommend Relay (full gossip participation), and mix delivery once it is available on current testnets; today Logos documents mix only for a testnet 0.2 chat build.
- Transaction shape. The room and on-chain paths differ; slot and chained-call counts are kept constant.
- The RPC endpoint sees the submitter's network address; we recommend a trusted or self-hosted endpoint, and submission over mix once it is available.
- The LEZ-to-chat binding. Signal: the signed BIND statement in the room plus the enrolment record in the member's mailbox. Why it remains: clients verify room approvals against enrolled keys. Inference: an outside observer of both layers learns nothing from the binding itself (timing aside, item 1); a member, or anyone holding a member's keys, can link that member's chat identity to their approval key and, through the enrolment commitment, to their LEZ account.
- Effects of public targets.
- Note sizes, including the members account's class.
- Creation time. The initialisation nullifier is
H(account_id), so anyone knowing an account id can date it. - The fee payer under PR 949 as it stands, until a program can pay from its own private account.
- The chat registry. Chat module v0.3.0 publishes and fetches key packages at one fixed server (
actions.rs), whose operator sees which installations look each other up, when, and from where.
Selective disclosure is per category: configuration (config and members account), holdings (each vault) and activity (the log). Group-derived keys are separate per program and seed, and the viewing secret is derived apart from the nullifier secret (group_key_holder.rs, secret_holders.rs), so each category's viewing key opens that category only. The audience receives the viewing key, account id and public key sealed to their keys, whether a named auditor or a DAO through its chat group. The viewer is in symeros-client (M3; exposed by the module in M4), since the wallet has no view-only import, and trial-decrypts new actions because notes carry random view tags.
The audience learns the disclosed categories' current state, full history and update times; for configuration, the approval keys (generated per multisig, so unlinkable to members' other keys); and the vault ids, which reveal this multisig's effects on public accounts. They cannot update or spend. History access cannot be withdrawn, and since viewing keys derive from the group secret, program and seed, revoking any audience needs a full key migration (§4); remaining audiences then receive new keys.
Ongoing assurance. A viewing-key holder can read the newest state at any time, and the multisig can publish periodic statements of configuration hash and holdings signed by M members.
Proof of holding (soft F.2), as research. The RFP treats this as not expressible with today's primitives; we think LEZ 0.3.1's existing circuit may express "this vault holds at least X" without a new circuit, and M3 tests it first. In one private transaction, a small immutable attestation program reads the vault's balance inside the proof without writing it and emits a public attestation of X tied to a salted hash of the vault id; the vault is re-committed unchanged, so the balance was current at settlement. Limits: point-in-time, producible by any holder of the vault's keys, and linkable to the multisig only with the salt or a disclosure. If it holds, M3 delivers a prototype; otherwise a design document.
7. Registry and decode-before-sign (F.4, U.6)
Entries. The registry is a public program mapping a target's program id to its image id, IDL hash and location, source repository and commit, and decoder version.
Verification.
- Verifiers attest an entry after a reproducible build reproduces the image id. Each config names the verifiers it trusts and how many must attest (two by default; clients flag a config that accepts one as not meeting F.4's "no single trusted source").
- Clients compare the entry's image id with the target's on-chain program header, so an IDL for different bytecode never reaches a signer.
- The approval digest includes the target's image id. Under the public posture
applychecks it against the header. Under the private posture that read would publish Symeros' program id, so the executor's client checks it before proving and every member's client compares it with the published image claim; multisigs that need the on-chain guarantee can restrict their action set to immutable targets. If LEZ adds a private way to bind a callee's image id, the check moves into the proof. - Clients sync the whole registry, so an RPC provider can't see which targets a multisig considers.
Decoding. The mini-app and CLI decode from the exact bytes signed: target, instruction and fields, accounts, amounts, vault. Symeros' own instructions have built-in decoders. Unregistered targets show the raw bytes, the program id and an "unregistered target" warning, and approval needs an explicit acknowledgement; they are not blocked, because F.3 allows any target. The mini-app ships as a signed module package from our catalog.
8. Attacks on our own design
"Blocked" means the attack fails in the program; otherwise the row says what we can do.
| Attack | Defence | Status | Negative test |
|---|---|---|---|
| Counterfeit 1-of-1 config spends the real vault | msid is the config's id; every seed derives from it; plan checks it |
Blocked | counterfeit-config spend fails |
| A group-key holder spends a vault directly | Only the Symeros program's seed grant debits a vault | Blocked | direct debit fails |
Executing with a queue entry from another config, or a different action under a queued seq |
Entries are leaves of queue_root committing to msid, seq, H(action) |
Blocked | foreign-entry and swapped-action executions fail |
| Re-executing a proposal | The config's replay window | Blocked | second execution fails |
| An open or early validity window skips the time lock | plan declares only bounded windows and copies them into the effect; apply derives eta from the queue window's end and checks the execute window against it |
Blocked | open-window and early executions fail |
| Two changes on the same epoch undo each other | Deltas with an expected epoch | Blocked | stale change fails |
| A removed member's approval decides a queued action | Removal migrates and voids the queue | Blocked | queued action fails after removal |
| A demoted executor keeps reading | Revoking execute migrates like removal | Blocked for new state; seen history stays seen (freezing: last row but one) | demoted keys open nothing in the successor |
| A compromised key's approval decides a queued action | Recount drops keys rotated after queueing | Blocked | rotated-key approval not counted |
| Limit approvals replayed, or used in another multisig | Per-limit counter; digest binds msid, limit, vault |
Blocked | replayed and cross-multisig spends fail |
| A wide window charges an old period | Window width and period checks | Blocked | straddling window fails |
| Target upgraded after signers decoded it | Image id in the digest; apply (public) or clients (private) check it |
Blocked (public); detected (private) | upgraded target fails; client refuses |
| Activity written to a twin log | Config pins members, limits and log ids | Blocked | twin-log execution fails |
| A twin vault handed out as deposit address | Vault ids in policy_hash; clients check |
Detected; funds are trapped, not stolen | client rejects an uncommitted id |
| A key holder, current or removed, re-consumes the config (freeze), including during migration | Group secret only with executors; no non-executor config touch; protocol rule proposed to LEZ in M1 | Bounded and detected, not blocked, on 0.3.1 | foreign consumption succeeds (documents the residual); non-executor config touch fails; detection |
| A foreign shard attached to a Symeros account | Same protocol rule would close it | Detected, not blocked, on 0.3.1 | detection |
9. Performance (P.1, P.2) and the pre-filing measurement
Baseline. LEZ's cycle_bench.md (Apple M2 Pro, CPU) reports about 4.4 s of proving per 64K-cycle bucket and privacy-circuit proofs around 225 KB, so with the 1 MiB maximum block size in LEZ's sequencer configurations roughly four private transactions fit in a block. Its padded transfer case (about 25 s) runs public accounts through the circuit, and its integration bench runs in dev mode, so LEZ publishes no real-proving time for a transaction over private accounts. Approval verification adds cycles in Symeros' apply, linear in M and step-wise in power-of-two buckets. Under the private posture the ceiling is client proving time and memory (there is no per-guest cycle cap). Under the public posture it is the 0.3.1 gas cap: gas equals cycles, the cap is 10M per transaction and per block (market.rs), and the wallet default is 2M, so our client sets the limit. At M = 8 the M + 2 signatures take about 2.7M, over a quarter of the cap.
What "viable" means for P.2 (our proposal; Logos may set others). Private: a full execute proves on a 16 GB laptop CPU under 12 GiB peak memory, the M approvals add less proving time than the same execute takes at M = 1, and it meets PR 949's fee window if that lands, using a GPU where needed. Times are reported for a desktop, a 16 GB laptop and a GPU where available. Public: within the 10M gas cap.
What we measured before filing. One guest apply verifying M signatures from distinct signers, over a 32-byte digest (Schnorr, P-256) or a 14-byte message (Ed25519). Proven for real with risc0-zkvm 3.0.5, the version LEZ 0.3.1 uses, on an AMD Ryzen 7 9800X3D (8 cores, 16 threads, CPU prover, 15 GiB RAM), one run per row. We used RISC Zero's accelerated k256 and p256 crates, which LEZ does not patch in, so M1 will confirm they work in a deployed program. The harness builds on fryorcraken/lez-signature-bench, which measured LEZ v0.2.0-rc3.
| Scheme | M | User cycles | Padded cycles | Proving (s) | Peak memory |
|---|---|---|---|---|---|
| no signatures | 1 / 8 | 7k / 14k | 65,536 | 5.8 / 5.9 | 0.6 GiB |
| BIP-340 Schnorr | 1 | 276,996 | 524,288 | 52 | 4.6 GiB |
| BIP-340 Schnorr | 5 | 1,357,215 | 1,572,864 | 136 | 9.2 GiB |
| BIP-340 Schnorr | 8 | 2,159,580 | 2,359,296 | 206 | 9.2 GiB |
| ECDSA P-256 | 1 | 203,604 | 262,144 | 22 | 2.3 GiB |
| ECDSA P-256 | 5 | 974,261 | 1,048,576 | 91 | 9.2 GiB |
| ECDSA P-256 | 8 | 1,547,069 | 2,097,152 | 182 | 9.2 GiB |
| Ed25519, RISC Zero fork | 1 | 904,711 | 1,048,576 | 90 | 9.2 GiB |
| Ed25519, RISC Zero fork | 8 | 7,161,917 | 7,864,320 | 672 | 9.2 GiB |
| Ed25519, crates as LEZ 0.3.1 resolves them | 8 | 28,058,686 | 29,622,272 | 2,529 | 9.2 GiB |
- Each Schnorr approval costs about 269k cycles and each P-256 approval about 193k. Ed25519 costs about 895k even with RISC Zero's fork, so we don't offer it.
- LEZ 0.3.1's own M-of-N check,
sequencer_stake::verify_approvals, uses plained25519-dalek2.2.0: about 3.5M cycles per signature in our harness, 3.9 times the fork. Stake transactions are fee-exempt and get the 32M default budget, not the 10M cap; about ten approvals exceed it. We will share this with the LEZ team. - These figures cover the verifying
applyonly;planadds about 22k cycles and 5.5 s. A full execute verifies M + 2 signatures (proposer and executor too) and adds the other applies, the target call and the privacy circuit's proof of each private account with composition. That part does not depend on M; we did not measure it and give no estimate. M1 measures one.
Harness, raw CSV and logs: vacuumlabs/symeros-bench.
Throughput. Every queue, execute and configuration change consumes the config, so a multisig settles at most one of them per full-execute proving time, which M1 measures; limited spends run alongside. Executors take turns through the room so proofs don't go stale. P.1 reports the measured figure.
The fee window. PR 949 as it stands prices private fees at a state at most 20 blocks old: about 200 s with the 10 s blocks in LEZ's docker and devnet configurations. The window applies whatever M is, and our apply at M = 5 already takes 136 s before the privacy circuit, so on a CPU our executes would likely miss it. If the window stays, they need GPU proving, which M1 measures where available.
During delivery. We prove for real on a desktop, a 16 GB laptop and a GPU where available. M1 repeats the measurement in the real apply on 0.3.1 within about a week and measures one full private execute end to end. M5 produces the final P.1 and P.2 reports on testnet 0.4: cycles, transaction size and proving time per operation, and P.2 at M = 1, 3, 5, 8 and 10 (each with M + 2 verifications) with the largest viable M against the thresholds above. If M ≥ 5 is not viable, we report it at once and, as P.2 provides, that opens a scope discussion. One option we would sketch, not build, is aggregating approvals into one proof while keeping R.1's per-member attribution.
10. Versions: 0.3 now, 0.4 at delivery, and the temporary 0.2 pieces
We build and test on LEZ v0.3.1 and testnet.lez.logos.co. Three ecosystem pieces are not on 0.3 yet:
| Gap | Meanwhile | Exit |
|---|---|---|
| SPEL has no 0.3 release; the 0.3 port is an unmerged branch (spel issue 291) | Program written against lee_core at LEZ tag v0.3.1; IDL in SPEL v0.7.0's schema, tested against the borsh types; fixes to the 0.3 branch's idl-gen sent upstream |
SPEL-generated IDL in M5 |
lez-programs token is on v0.2.4; its 0.3 upgrade is a WIP branch |
Native token on 0.3; token-bound limits tested in CI against a test token with the same transfer interface, or the WIP branch | The published token program, in M5 |
lez-multisig is on 0.2 and does not meet R.2 |
A source of public-path test cases | n/a |
Prior art. λ prize LP-0002's solution merged in September 2026 (lambda-prize PR 144): approvals are nullifiers, so the chain counts rather than attributes them, anyone may execute at M, and the member set is fixed. It offers neither R.1 nor R.2.
S.3 as written requires both 0.3 and 0.4, and that is our baseline. Until rfp PR 214 is merged we keep the 0.3 adapter building and tested in CI next to the 0.4 adapter, outside the audit, which covers 0.4 only (§13); if it is merged, the 0.3 adapter stops at M5, with Logos's agreement. Every acceptance test also runs in CI against a standalone sequencer, so milestone evidence survives the 0.3 testnet reset; testnet links are a snapshot.
0.4. Ecosystem issue 251 says 0.4 resets the testnet, changes the programming model and splits the core module. LEZ PR 935 ("transactional actors"), merged on 14 October, shows the likely shape; it also changes identifiers and how private accounts are initialised. M5 ports the Symeros, registry and (if delivered) attestation adapters, the client, the modules and the mini-app (0.4 splits the core module) and room provisioning, then redeploys and re-measures, under the assumptions listed in Milestones. If 0.4 is ready earlier, the port starts in the milestone then running (see Milestones), and the 0.3 build stays in CI as S.3 requires.
11. Clients, documentation and errors (U.1–U.3, U.7, S.8–S.12)
symeros_module exposes create; propose, approve, reject; queue and execute; configuration changes and policies; room operations; and disclosure. Proving runs as a background job with progress events. The CLI is a logosctl-driven module covering the full operator journey. The Basecamp mini-app (ui-qml-backend) shows the proposal list beside the room, decodes before signing, and has a disclosure screen stating what each audience sees; QML holds no business logic.
Documentation and distribution (S.8–S.12): a README with build and deployment steps, program addresses, and a create, propose, approve and execute walkthrough via CLI and mini-app, including room provisioning; API documentation for symeros_module, CLI documentation for every command, Figma designs, and the per-role guarantee documentation (F.6). Modules and the mini-app ship as packages from our module catalog, built from the Logos catalog template, with local build instructions.
Errors (U.7). Clients execute the program locally before proving, so a failed check is reported in seconds as an instruction, before minutes of proving: "member 3 approved under epoch 7; the multisig is now at epoch 8; ask member 3 to approve again." Public-posture transactions use typed exit codes (PR 837) with the same messages.
12. Testing (S.5–S.7)
symeros-core: property tests over generated member sets, epochs, approval sets and limit windows. Adapter: LEZ's program test harness. CI: end-to-end tests against a standalone sequencer on every push, plus a scheduled real-proving job for P.1 and P.2.- S.7: at least one test per hard requirement, including the RFP's three: execution below threshold fails; R.2 (pre-queue epoch invalidation, voiding by removal, recount and suspension after a weight or threshold change,
void_queue); a spend before initialisation fails. - U requirements: U.1 an integration test calling every module method; U.2 a Basecamp smoke test rendering the decode screen for a fixture; U.3 the CLI end-to-end script; U.5 a test checking the documented public fields against the layouts the client builds; U.6 decoding from signed bytes; U.7 every failure path maps to a message.
- Every attack in §8 has its own negative test.
13. Audit (S.13)
- Firm: Zellic or Sherlock, with OpenZeppelin as the alternate; quotes requested in M1.
- Scope: the Symeros program, approval verification, configuration change, migration and R.2 logic, time lock, spending limits, the registry program,
symeros-core, the attestation program if delivered, and the 0.4 adapters. The 0.3 adapter is not audited: the audit starts only once the M5 port to 0.4 is frozen. - Timeline: booked in M1 to start after M5, around week 21 (late March 2027 for a November 2026 start), after Logos's own audits in late February or March (ecosystem issue 251).
- Budget: a $50,000 line item (M6), outside the fixed price. We engage and pay the auditor and bill the audit at cost, with no margin. If the best quote in M1 is higher, we agree the difference or a narrower scope with Logos before engaging; any unspent amount is not claimed. Our remediation is free of charge (M7).
- Publication: the report is published with the code in M7, before any production deployment or mainnet recommendation.
14. Design decision log (S.14)
Kept in Symeros' public repository, which we create in M1, and current at every milestone. Its first entries record the choices argued for in §2–§10, with their options and trade-offs.
15. Requirements traceability
| Req | Where | Milestone | Test or evidence |
|---|---|---|---|
| F.1 M-of-N, set at creation | §2, §4 | M2 | create variants; action outside the set fails |
| F.2 Lifecycle, expiry, collection | §3, §4, §5 | M2; collection M3 | expired proposal fails; room and on-chain paths both execute |
| F.3 Any target program | §4 | M2 | native token, test token, registry program |
| F.4 Registry, unregistered display | §7 | M2, M4 | image-id mismatch rejected; acknowledgement required |
| F.5 Config changes through quorum | §4 | M2 | delta changes; stale change fails |
| F.6 Role separation | §3, §4 | M2 | each role enforced on both paths; per-role guarantee documentation |
| F.7 Time lock | §4 | M2 | early execution fails; zero delay executes |
| F.8 Spending limit | §4 | M2 | over-cap, replay and window tests |
| F.9 Room and binding | §5 | M3 | provisioning, binding, re-binding, rotation |
| F.10 Postures, disclosure | §6 | M2, M3 | both postures deployed; one category's key opens nothing else |
| U.1 Module API | §11, §12 | M4 | integration test over every method; API docs |
| U.2 Mini-app with room | §11, §12 | M4 | Basecamp smoke test; local build and packages |
| U.3 CLI | §11 | M4 | CLI end-to-end script |
| U.4 IDL | §10 | M2, M5 | IDL checked against borsh types |
| U.5 Public/private documentation | §6, §12 | M3 | layout-check test; per-transaction, per-posture tables |
| U.6 Decode before sign | §7 | M4 | decoding from signed bytes |
| U.7 Actionable errors | §11 | M4 | every failure mapped to a message |
| R.1 Valid approvals | §3 | M2 | duplicate, non-member, wrong action, replay |
| R.2 Invalidation | §4 | M2 | the four R.2 tests (§12) |
| P.1, P.2 | §9 | M1, M5 | benchmark reports and harness |
| S.1, S.2 | §2, §5 | M2, M3 | covered by the design itself |
| S.3, S.4 Testnets | §10 | M2, M5 | deployments on 0.3 and 0.4 |
| S.5–S.7 CI and tests | §12 | from M1 | green CI on the default branch |
| S.8–S.11 Docs and designs | §11 | M4 | README, API and CLI docs, Figma |
| S.12 Module catalog | §7, §11 | M4 | team catalog from the template |
| S.13 Audit | §13 | M6, M7 | auditor's report (M6), published with fixes (M7) |
| S.14 Decision log | §14 | all | decision log in the public repository |
| Privacy 1–3 | §6 | M3 | privacy and correlation documents |
| Soft F.1 Weighted approvals | §3 | M2 | weight tests |
| Soft F.2 Proof of holding | §6 | M3 | feasibility test; prototype or design |
| Soft F.3 Mixed postures | §6 | M3 | two combinations deployed; what each reveals (§6) |
Milestones, Payout and Timeline
Uncertainty. The RFP names approval-verification cost as its largest unpriced item. We measured each approval's cost before filing (§9). Within its fixed price, M1 measures a full execute, GPU proving for PR 949's fee window, the real apply's overhead and whether RISC Zero's accelerated crates work in a deployed LEZ program; if M ≥ 5 is not viable we say so at once, opening the RFP's scope discussion. M5's stated assumptions cover 0.4.
Ramp-up. Mostly done and unbilled: before filing we worked through LEZ 0.3.1's authorisation rules and note format and measured signature verification on its prover stack. The remaining engineer-week, on the deployment path and testnet tooling, is inside M1.
Schedule. Start date as Logos sets it. About 27 weeks, with the 0.4 port (M5) inside that time and the total price. M3 (room, on-chain path, disclosure) and M4 (module, CLI, mini-app, designs) run in parallel on separate engineers. We move to 0.4 once testnet 0.4 is live on a tagged LEZ release and SPEL, the token program and the wallet FFI have a release or usable branch, not at a fixed week. If that is before M5, we port in the milestone then running, which is then accepted on 0.4; M5 keeps the rest of the port, the final 0.4 measurements and the freeze, so it and the calendar shorten. If porting mid-milestone would risk its acceptance, we finish it on 0.3 and port at its end. We tell Logos when we move; moving early changes no payout or total. The weeks below assume 0.4 is ready by week 16.
What M5's price assumes. Porting the adapters, the client, the modules and the mini-app to 0.4, whatever its programming model, provided 0.4 still offers (1) program-controlled private accounts, (2) group-derived keys for them, (3) signature verification inside a program, (4) a program seeing the id of each private account it is given, and (5) a private-transaction claim that hides which immutable program ran. If 0.4 removes one, we say so within a week of its release and propose the closest equivalent design; the price does not rise. If 0.4 is not ready by week 16, M1–M4 are accepted and paid as normal, we keep the 0.3 build current at no extra cost, and the audit booking moves with M5. The audit is of the 0.4 program only, so M6 and M7 wait for the 0.4 port; the wait costs Logos nothing.
M1: Foundations and feasibility (weeks 1–3). Payout: $17,000
- Deliverables:
symeros-corewith property tests; a minimal program slice on 0.3.1 (planandapplyover the config and one vault, with themsidcheck) deployed on a standalone sequencer and testnet; in that slice, the approval-verification benchmark, one full private execute measured end to end on a desktop and a 16 GB laptop, with GPU proving where available, and negative tests for direct spend and counterfeit config; the foreign-consumption test and detection, and the protocol-rule proposal to the LEZ team; the wallet-FFI coverage check; threat model v1 and decision log v1, published; audit quotes and booking. - Acceptance: public repository with green CI, the benchmark report with real proving, the decision log.
M2: Multisig program on LEZ 0.3 (weeks 4–9). Payout: $34,000
- Deliverables: F.1–F.8 and R.1–R.2 with weighted approvals (for F.2, executing with approvals supplied directly; room and mailbox collection lands in M3); mailbox enrolment; the replay window and
queue_root; removal migration; per-role guarantee documentation (F.6), including the executor's freeze power; the registry program; the public posture; the initialisation guard; the IDL; end-to-end CI against a standalone sequencer; testnet deployment. - Acceptance: program addresses, a test per requirement, linked testnet transactions.
M3: Coordination room and privacy (weeks 10–14). Payout: $28,000
- Deliverables: join cards, provisioning, binding and re-binding, the room protocol, and room rotation after a removal migration; room-collected approvals and the on-chain path; per-category disclosure, the viewer in
symeros-client, and assurance statements; mixed postures; Privacy 1–3 and U.5 documentation; proof of holding (feasibility test, then a prototype or design); a shadow-execution test for the private posture. - Acceptance: a private end-to-end flow on testnet with approvals collected in the room; the privacy documentation.
M4: Module, CLI and mini-app (weeks 10–15). Payout: $33,000
- Deliverables:
symeros_moduleand the CLI; the Basecamp mini-app with room, proposal list, decode-before-sign and disclosure screen; wireframes, then Figma designs; module catalog; README, API and CLI documentation. - Acceptance: modules as downloadable release assets, installable from our catalog and loadable in Basecamp via git repo; documentation in the repository.
M5: Testnet 0.4 (at most 5 weeks after M4 once 0.4 is ready, about weeks 16–20; shorter if the port started earlier). Payout: $28,000
- Deliverables: 0.4 port of the Symeros, registry and (if delivered) attestation adapters, the client, the modules, the mini-app and room provisioning, and redeployment; final P.1 and P.2 on 0.4; SPEL IDL and token program re-targeting; code freeze and audit package.
- Acceptance: testnet 0.4 deployment exercised through the CLI and mini-app, benchmark report, frozen commit.
M6: Audit (4 weeks after M5, about weeks 21–24). Payout: $50,000, billed at cost (§13)
- Deliverables: tier-1 audit of the frozen commit. Acceptance: the auditor's report.
M7: Remediation and release (3 weeks after M6, about weeks 25–27). Payout: none; free of charge
- Deliverables: fixes and auditor fix review; the published report; release; final deployment on testnet 0.4, with its addresses in the README.
- Acceptance: published audit report and release tag.
Total Requested Budget (USD)
$190,000: $140,000 fixed for M1–M5, plus the $50,000 audit line (M6) billed at cost; remediation free of charge
Relevant Experience
Vacuumlabs has built hardware-wallet signing since 2018, the skill U.6 is written for. Bybit and WazirX signers approved what a screen showed, not what they signed. A hardware-wallet app decodes the exact bytes it signs, on a screen the host cannot touch.
- Cardano on Ledger and Trezor. We built the Cardano Ledger app (LedgerHQ/app-cardano is a fork of ours), Cardano support in Trezor firmware, and the client stack: ledgerjs-cardano-shelley, cardano-hw-interop-lib and cardano-hw-cli. The devices decode each transaction, including multisig ones where the device signs as one member of a quorum, and keep payment, stake, multisig and governance keys on separate paths: F.6's role separation.
- Maintained through protocol upgrades. As an Intersect Cardano continuity supplier we keep both devices working across upgrades such as CIP-1694 governance. Until the decoder learns a new transaction type, the client must decide what to show for bytes it cannot fully interpret: F.4's problem, and calls we have defended to vendors' security reviews.
- Solana on Trezor (2023, with NuFi). On-device decoding of Solana instructions, payloads much like LEZ actions, in Trezor firmware, and the host flow in Trezor Suite.
- Custody front ends. AdaLite (Ledger, Trezor, BitBox02) and Yoroi mobile with EMURGO, where signing is the critical path, as in U.2.
Against the RFP profile. Multisig and custody: multisig signing and role-separated keys on devices. Rust and zkVM: our Rust work in dcSpark's Sovereign SDK rollup and the RISC Zero benchmark in §9. Applied cryptography: Cardano signing in AdaLite and firmware. Signing UX and hardware wallets: above. Front end: AdaLite, Yoroi, Trezor Suite.
Post-Delivery Plan
Vacuumlabs maintains Symeros as a team, not through one person. We ask Logos for no funding after delivery.
Twelve months of maintenance, unbilled. It is not part of any milestone or of the requested budget; Vacuumlabs carries the cost. From acceptance of the final milestone:
- Defect fixes.
- Security reports. Acknowledged within 24 hours, triaged within 72.
- Dependency updates.
- Compatibility with Logos testnet releases in that period.
- One port for a release that changes the programming model, as a new versioned deployment with migration notes, within eight weeks of the release or of final acceptance, whichever is later. We expect it to be the private-UTXO release after 0.4 (ecosystem issue 251), and it is covered whether that release ships during the build or after it. Ports for releases that don't change the programming model are not counted.
- A port that changes audited code is marked unaudited until it is reviewed.
After that, the code, decision log and runbook let any team maintain it. There is no fee in the audited program and no token.
Handover artifacts: the decision log and threat model, the audit report and remediation record, the deployment runbook and benchmark harness, and the module catalog.
All code is MIT and Apache-2.0.
Permissions and Consent
- I/We confirm Logos may contact me using the primary contact information provided above for follow-ups and next steps.
- I/We consent to Logos using information from this proposal publicly such as blogs case studies social posts or analytical reporting. Redactions can be requested at any time.
Program Requirements
- I/We have read and agree to the Logos RFP Terms and Conditions and I understand that no Grant is awarded and no right to payment arises unless a Grant Agreement is executed.
- I/We understand that RFP specifications are proposals rather than instructions, that Logos makes no representations as to their legal or regulatory treatment, and that we are responsible for assessing what we build, deploy or operate and for complying with the laws that apply to us.
- I/We understand this project must be open-sourced under the MIT and Apache 2.0 Licenses unless explicitly approved otherwise.
- I/We are prepared to deliver milestone-based outcomes.
Activity
[-][PROPOSAL] RFP-005 — Quorate[/-][+][PROPOSAL] RFP-005 — Symeros[/+]Soulrealz commentedon Oct 16, 2026
Note the last edits were purely to rename the project. Nothing else apart from the name was changed.