AlphaCOLLAB runs on GenesisL1 mainnet and has had no external audit. Use small amounts.Security notes

COLLAB

Governance internals

This page is for developers who write proposal builders, add policy rules, review COLLAB's governance or check a proposal by hand. It explains how DAOPolicy decides what each proposal kind may do and whether the stricter critical rules apply, what PolicyBuilder gives every new DAO, how the governor counts votes and how a proposal commits to its actions. For the members' view, read Governance rules and presets and The 13 proposal types.

Part
Developers
Version
COLLAB 2.2.0
Updated
Reading time
12 min

Proposal kinds

Every proposal declares a kind (Kinds in contracts/interfaces/ICollab.sol). The kind selects the rules its actions must satisfy.

IdConstantName in the appCritical by default
0SIGNALSignalnever: no actions, always standard rules
1TREASURYTreasury spendno
2PARAMSGovernance rulesalways
3CONSTITUTIONConstitutionalways
4PROFILERepresentatives & profileno
5MEMBERSHIPMembershipalways
6TOKEN_ISSUANCEIssue a tokenno
7TOKEN_ADMINManage a tokenno
8DESCIFIDeSciFi liquidityno
9CIPNFTSealed data (CIPNFT)no
10GL1FModels (GL1F)no
11SIGNATUREDAO signaturealways
12CUSTOMRaw transactionalways

criticalKinds is a bit mask (bit k for kind k). MANDATORY_CRITICAL holds PARAMS, CONSTITUTION, MEMBERSHIP, SIGNATURE and CUSTOM, and setCriticalKinds(mask) always adds it, so those five can never become standard. A DAO adds more at creation (extraCriticalKinds) or by a Governance rules proposal. A Signal proposal carries no actions (SignalHasActions) and validate returns standard for it before looking at the mask. A Raw transaction may call anything except the zero address and is always critical.

Rules

A rule is Rule {uint8 kind; address target; bytes4 selector; uint8 flags}:

FlagValueMeaning
RULE_ALLOWED1the call is allowed
RULE_VALUE2the call may carry L1
RULE_CRITICAL4using this call makes the proposal critical

A rule is stored under keccak256(abi.encodePacked(kind, target, selector)) together with an internal "explicit" bit, so a rule set with flags 0 is an explicit deny, which is not the same as no rule. Signal and Raw transaction kinds take no rules (UnknownKind). The target is one of:

  • a contract address: a rule for that contract only;
  • ANY_TARGET, address(1): the kind's wildcard rule;
  • a role target, roleTarget(role) = address(0x100 + role): 0x101 every CIPNFT contract the DAO lists, 0x102 every listed GL1F registry, 0x103 Model NFT, 0x104 market, 0x105 runtime.

No kind but Raw transaction may target an address at or below 0x105 (InvalidTarget(index)): the precompiles and the rule keys are never contracts.

Precedence: explicit, then role, then wildcard

For an action on target with selector in a proposal of kind, the policy looks up, in order:

  1. the explicit rule for exactly (kind, target, selector), a deny included;
  2. if the target holds an integration role, the rule for (kind, role target, selector);
  3. the kind's wildcard rule (kind, ANY_TARGET, selector), but only where the wildcard reaches.

A wildcard reaches a contract whose home is the proposal's own kind. A contract without a home is reached only by the wildcards of Treasury spend, Issue a token and Manage a token, and by approve in any kind, since an approval always names an approved spender. A wildcard never reaches a contract that belongs to another kind.

clearRule(kind, target, selector) removes an explicit rule, so the role's or the wildcard rule applies again. The views ruleOf (the flags of the exact rule, 0 when unset or a deny), ruleSet (whether it is set, and its flags), effectiveRule (what applies now, with the roles listed now) and homeKind(target, selector) let a client show a rule change before and after.

Homes

A home is the kind a contract belongs to:

ContractHome
the DAO's profilePROFILE
the DAO's key vault, and every CIPNFT contract the DAO listsCIPNFT
every contract of a listed GL1F collectionGL1F
a token this DAO launchedTOKEN_ADMIN, except transfer, approve and transferFrom, which have no home (treasury business)
a DeSciFi poolDESCIFI, except transfer, approve and transferFrom (LP tokens are assets)
a DeSciFi lock boxDESCIFI
anything elsenone

An action on a contract whose home is another kind makes the proposal as critical as that kind makes it: critical when the home kind is critical, or when the home kind's own rule for that call is marked critical. So a DeSciFi liquidity proposal acts only on real pools and lock boxes, a Manage a token proposal cannot burn a sealed asset, and a Treasury spend cannot move a Model NFT.

Integrations and roles

addGl1fCollection gives its four contracts the roles 2 to 5 and makes its market an approved GL1F spender; addCipnft gives a contract role 1. A contract is listed only if it has code, holds no role yet, and is none of the DAO's own contracts and none of the protocol contracts its factory knows (InvalidIntegration("DAO contract" | "protocol contract" | "no code" | "already listed" | …)), at most 8 of each. Default rules are written for role targets, so they apply to every integration the DAO adds. A proposal's later actions may already use a contract an earlier action of the same proposal lists, so one proposal can add a CIPNFT contract and register the DAO's key there. A removed contract keeps no role: the assets left there fall to the rules that reach contracts without a home.

Critical, whatever the kind

Besides the kind's own status and rules flagged critical, these make a proposal critical:

  • any action on the DAO's governor, treasury, policy or share token;
  • an action on a contract whose home kind is critical, or whose home kind's rule for the call is critical;
  • transferOwnership or renounceOwnership of a contract without a home;
  • setRepresentatives that gives anyone PERM_OPERATOR (32).

Value and approvals

  • An action with empty data is a plain L1 payment: only a Treasury spend may make one, and only with value (ActionNotAllowed(index, target, 0x00000000)). Data of 1 to 3 bytes is refused.
  • An action with value needs RULE_VALUE (ValueNotAllowed(index)).
  • approve and setApprovalForAll must name a spender approved for the proposal's kind (approvedSpender[kind][spender], SpenderNotAllowed(index, spender)), unless they name no one: approve(address(0), …) (clearing an NFT approval) and setApprovalForAll(x, false). The spender word must be a clean address.

The default spenders: the distributor for Treasury spend; the launcher for Issue a token; the router, the DeSciFi factory and the lock factory for DeSciFi liquidity; the market of every listed GL1F collection for Models.

The treasury stance

When Treasury spend is critical in a DAO (bit 1 of criticalKinds), every action that can give up treasury value is critical too, whatever the proposal's kind (_givesUpValue). That is an action that:

  • carries L1;
  • grants an allowance: approve of a non-zero amount to a non-zero spender (LP tokens of DeSciFi pools excepted; on a listed GL1F Model NFT any token id, 0 included), or setApprovalForAll(operator, true);
  • trades or sells at a price the proposal sets: the router's six swaps, a pool's own swap, addLiquidity, addLiquidityL1, createLaunchPool, TokenLauncher.launch, list on a listed GL1F market, updateModelSettings, createAccessPlan, setAccessPlan and setOwnerAccessKey on a listed GL1F registry, opening a listing on a listed CIPNFT contract, and CIPNFT's deliverOffer and finalizeOffer called directly (the key vault's checked deliverSale and finalizePurchase are the usual way);
  • sends assets, fees, LP or rights to anyone but the treasury: transfer, mint(to, amount) and transferOwnership to another address, the transferFrom and safeTransferFrom forms, removeLiquidity and removeLiquidityL1 paying elsewhere, and a pool's collectFees, mint, burn or skim, or a lock box's withdraw, paying elsewhere;
  • locks LP for another owner or fee recipient (the burn address included), or beyond LOCK_HORIZON (3,650 days from the proposal's submission) or forever, in a new lock box or by extending one;
  • gives up what a token is worth or can do: burn(amount), renounceOwnership, or renounceCapability of anything but pausing (flag 4).

Recipients and dates are read at their exact ABI position, and calldata too short to hold one counts as naming someone else, so a misread can only make a proposal critical. The stance knows the protocol's calls and those of listed integrations. A rule that a Governance rules proposal adds later for another call that moves value, or for a contract the DAO has not listed, is standard unless the rule itself is marked critical: mark such rules critical.

validate()

validate(uint8 kind, Action[] actions) view returns (bool critical) reverts when the actions are not allowed for the kind and otherwise says whether the critical rules apply. The governor calls it in submitProposal; any client can call it to check a draft, as the app's composer does. The steps:

  1. A kind of 13 or more reverts UnknownKind(kind).
  2. Signal: actions revert SignalHasActions(); otherwise it returns false.
  3. No actions revert NoActions().
  4. A zero target, or (except for Raw transaction) a target at or below 0x105, reverts InvalidTarget(index). Actions that list an integration on this policy are noted, so later actions may use what they list.
  5. Raw transaction returns true.
  6. critical starts as the kind's bit in criticalKinds. Then, for each action: the DAO's own core contracts make it critical; a plain payment is checked; the target's role (now, or from an earlier listing in the same proposal) and home are found; the rule is looked up by precedence and must allow the call (ActionNotAllowed(index, target, selector)), and its value (ValueNotAllowed); flagged, foreign-home, ownership, operator and treasury-stance cases make it critical; approvals are checked against the spenders (SpenderNotAllowed).
  7. It returns critical.

effectiveRule is the per-call view of step 6, without a proposal's own earlier listings.

The 115 default rules

PolicyBuilder.build gives every new DAO these rules (2.1 had 116: the title rule is gone, since titles are permanent) and five approved spenders. "Every listed …" means the role targets.

KindRulesWhat they allow
Treasury spend11transfer, transferFrom, both ERC-721 safeTransferFrom, ERC-1155 safeTransferFrom and safeBatchTransferFrom, approve and setApprovalForAll on any contract (spender-restricted); createCampaign on the distributor (with L1); wL1 deposit (with L1) and withdraw
Governance rules18, all criticalthe governor's setParams and setHoldingPeriod; the policy's setRule, setRules, clearRule, setApprovedSpender, setCriticalKinds, addGl1fCollection, removeGl1fCollection, addCipnft, removeCipnft; the treasury's setModulePermission; the shares' setTransfersEnabled and setExcluded; acceptTos, setEncryptionPublicKey and setPqcPublicKey on every listed CIPNFT contract, and the vault's activateKeyEpoch (so one proposal can list a contract, register the key and rotate it)
Constitution1, criticalthe profile's amendConstitution
Representatives & profile6the profile's setRepresentatives, removeRepresentative, setDescription, setLogo, setLinks, announce
Membership5, all criticalthe shares' freeze, unfreeze, mint, burn and transfer
Issue a token3the launcher's launch (with L1); approve on any contract (spender: the launcher); wL1 deposit (with L1)
Manage a token8on any token the kind reaches: mint, pause, unpause, renounceCapability, setMetadata, transferOwnership, renounceOwnership, burn
DeSciFi liquidity21the router's addLiquidity, addLiquidityL1 (with L1), removeLiquidity, removeLiquidityL1 and six swaps (the two that spend L1 with L1); the factory's createPair and createLaunchPool; the lock factory's createLock; on any lock box withdraw, extendLock, collectFees, transferOwnership; on any pool collectFees; approve (spenders: router, factory, lock factory); wL1 deposit (with L1) and withdraw
Sealed data (CIPNFT)23on every listed CIPNFT contract acceptTos, mint and createOffer (with L1), cancelOffer, setListing, revokeDelivery, refundExpiredOffer, setTitle, and, critical, setEncryptionPublicKey, setPqcPublicKey, finalizeOffer, deliverOffer, setViewWrap, updateOwnerEncDEK, burn, eraseTokenData; the vault's closeReveal, approvePurchase, revokePurchase and, critical, setStewards, activateKeyEpoch, openReveal, openDeclassification
Models (GL1F)17on every listed registry updateModelSettings, createAccessPlan, setAccessPlan, setOwnerAccessKey, revokeAccessKey, buyAccess (with L1) and, critical, burnAndDelete, changeModelLicense, setInternalsVisibility; on every listed Model NFT approve and setApprovalForAll (spenders: the listed markets), transferFrom and both safeTransferFrom; on every listed market list and buy (with L1) and cancel
DAO signature2, criticalthe treasury's signHash and revokeHash

The Manage a token rules recognise functions by signature, so they take effect only where the DAO may call them: on tokens it owns, and under the critical rules on its own shares, whose mint and burn share those signatures.

How proposals commit to their actions

  • submitProposal stores keccak256(abi.encode(actions)) and nothing else of them; the full actions are in the ProposalSubmitted event. A proposal without actions stores EMPTY_ACTIONS_HASH, and when it passes it stays Passed: there is nothing to queue or execute.
  • execute(id, actions) hashes what it is given and reverts ActionsMismatch() unless it equals the stored hash. Execution is permissionless: whoever executes chooses the moment within the window, never the content.
  • The kind, the critical decision, a copy of the rules that apply, the deposit flags, the minimum deposit and the execution window are stored in the proposal at submission, so a rule change never affects a proposal already in flight.
  • A client reads the actions from the ProposalSubmitted log in the proposal's createdBlock, checks them against actionsHash (the proposal page does it with proposalActions and actionsHash from web/js/core/actions.js), decodes and describes them, and can simulate them: once the proposal is Executable, an eth_call of governor.execute(id, actions), exactly the transaction anyone may send; before that, treasury.executeBatch(actions) from the governor, without the governor's context.
  • A Membership proposal records the accounts its freeze actions target at submission, refusing excluded ones (FreezeTargetExcluded). With FLAG_FREEZE_LOCK their shares are locked from the start of the vote until one second after the last moment the proposal could execute; anyone may releaseLocks(id, actions) once it can no longer execute (it failed, was executed or its window closed).

How votes are counted

The snapshot is the block before submission. With a holding period, a second reading is taken holdingBlocks earlier (never before the DAO's creation block), and every voting power counts the lower of the two readings, so shares bought inside the window add nothing. A delegate votes with what it was lent; a delegator who votes directly takes its own units back from its delegate at each reading, before or after the delegate votes.

With totalPower = getPastTotalUnits(snapshot) and voted = yes + abstain + no + veto, the outcome after voting ends is the first that applies:

  1. No quorum, when voted is 0 or voted × 10,000 < quorumBps × totalPower;
  2. Vetoed, when veto × 10,000 > vetoThresholdBps × voted;
  3. Rejected, when nobody voted other than Abstain;
  4. Rejected, unless yes × 10,000 > thresholdBps × (voted − abstain) (or the threshold is 100 % and every non-abstaining vote is Yes);
  5. Rejected, when yes × 10,000 < minYesBps × totalPower;
  6. otherwise Passed.

_checkRules bounds every rule set: voting period 10 minutes to 365 days, timelock at most 60 days, quorum at most 100 %, threshold 50 % to 100 %, veto threshold above 0, minimum Yes at most 50 %. The critical set must be at least as strict as the standard one in each: quorum, threshold, timelock, voting period and minimum Yes no lower, veto threshold no higher.

Deposits are refunded (claimDeposit) or forfeited (settlePenalty) by the outcome and the flags copied at submission: a deposit period that expired or a withdrawn proposal forfeits with FLAG_BURN_DEPOSIT_PREVOTE; No quorum with FLAG_BURN_VOTE_QUORUM; Vetoed with FLAG_BURN_VOTE_VETO; a freeze proposal that ends Rejected or No quorum with FLAG_FREEZE_BOND. Forfeits go to 0x…dEaD, or to the treasury with FLAG_PENALTY_TO_TREASURY.