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.
Proposal kinds
Every proposal declares a kind (Kinds in contracts/interfaces/ICollab.sol). The kind selects the rules its actions must satisfy.
| Id | Constant | Name in the app | Critical by default |
|---|---|---|---|
| 0 | SIGNAL | Signal | never: no actions, always standard rules |
| 1 | TREASURY | Treasury spend | no |
| 2 | PARAMS | Governance rules | always |
| 3 | CONSTITUTION | Constitution | always |
| 4 | PROFILE | Representatives & profile | no |
| 5 | MEMBERSHIP | Membership | always |
| 6 | TOKEN_ISSUANCE | Issue a token | no |
| 7 | TOKEN_ADMIN | Manage a token | no |
| 8 | DESCIFI | DeSciFi liquidity | no |
| 9 | CIPNFT | Sealed data (CIPNFT) | no |
| 10 | GL1F | Models (GL1F) | no |
| 11 | SIGNATURE | DAO signature | always |
| 12 | CUSTOM | Raw transaction | always |
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}:
| Flag | Value | Meaning |
|---|---|---|
RULE_ALLOWED | 1 | the call is allowed |
RULE_VALUE | 2 | the call may carry L1 |
RULE_CRITICAL | 4 | using 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):0x101every CIPNFT contract the DAO lists,0x102every listed GL1F registry,0x103Model NFT,0x104market,0x105runtime.
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:
- the explicit rule for exactly (kind, target, selector), a deny included;
- if the target holds an integration role, the rule for (kind, role target, selector);
- 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:
| Contract | Home |
|---|---|
| the DAO's profile | PROFILE |
| the DAO's key vault, and every CIPNFT contract the DAO lists | CIPNFT |
| every contract of a listed GL1F collection | GL1F |
| a token this DAO launched | TOKEN_ADMIN, except transfer, approve and transferFrom, which have no home (treasury business) |
| a DeSciFi pool | DESCIFI, except transfer, approve and transferFrom (LP tokens are assets) |
| a DeSciFi lock box | DESCIFI |
| anything else | none |
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;
transferOwnershiporrenounceOwnershipof a contract without a home;setRepresentativesthat gives anyonePERM_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)). approveandsetApprovalForAllmust 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) andsetApprovalForAll(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:
approveof 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), orsetApprovalForAll(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,liston a listed GL1F market,updateModelSettings,createAccessPlan,setAccessPlanandsetOwnerAccessKeyon a listed GL1F registry, opening a listing on a listed CIPNFT contract, and CIPNFT'sdeliverOfferandfinalizeOffercalled directly (the key vault's checkeddeliverSaleandfinalizePurchaseare the usual way); - sends assets, fees, LP or rights to anyone but the treasury:
transfer,mint(to, amount)andtransferOwnershipto another address, thetransferFromandsafeTransferFromforms,removeLiquidityandremoveLiquidityL1paying elsewhere, and a pool'scollectFees,mint,burnorskim, or a lock box'swithdraw, 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, orrenounceCapabilityof 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:
- A kind of 13 or more reverts
UnknownKind(kind). - Signal: actions revert
SignalHasActions(); otherwise it returnsfalse. - No actions revert
NoActions(). - A zero target, or (except for Raw transaction) a target at or below
0x105, revertsInvalidTarget(index). Actions that list an integration on this policy are noted, so later actions may use what they list. - Raw transaction returns
true. criticalstarts as the kind's bit incriticalKinds. 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).- 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.
| Kind | Rules | What they allow |
|---|---|---|
| Treasury spend | 11 | transfer, 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 rules | 18, all critical | the 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) |
| Constitution | 1, critical | the profile's amendConstitution |
| Representatives & profile | 6 | the profile's setRepresentatives, removeRepresentative, setDescription, setLogo, setLinks, announce |
| Membership | 5, all critical | the shares' freeze, unfreeze, mint, burn and transfer |
| Issue a token | 3 | the launcher's launch (with L1); approve on any contract (spender: the launcher); wL1 deposit (with L1) |
| Manage a token | 8 | on any token the kind reaches: mint, pause, unpause, renounceCapability, setMetadata, transferOwnership, renounceOwnership, burn |
| DeSciFi liquidity | 21 | the 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) | 23 | on 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) | 17 | on 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 signature | 2, critical | the 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
submitProposalstoreskeccak256(abi.encode(actions))and nothing else of them; the full actions are in theProposalSubmittedevent. A proposal without actions storesEMPTY_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 revertsActionsMismatch()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
ProposalSubmittedlog in the proposal'screatedBlock, checks them againstactionsHash(the proposal page does it withproposalActionsandactionsHashfromweb/js/core/actions.js), decodes and describes them, and can simulate them: once the proposal is Executable, aneth_callofgovernor.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
freezeactions target at submission, refusing excluded ones (FreezeTargetExcluded). WithFLAG_FREEZE_LOCKtheir shares are locked from the start of the vote until one second after the last moment the proposal could execute; anyone mayreleaseLocks(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:
- No quorum, when
votedis 0 orvoted × 10,000 < quorumBps × totalPower; - Vetoed, when
veto × 10,000 > vetoThresholdBps × voted; - Rejected, when nobody voted other than Abstain;
- Rejected, unless
yes × 10,000 > thresholdBps × (voted − abstain)(or the threshold is 100 % and every non-abstaining vote is Yes); - Rejected, when
yes × 10,000 < minYesBps × totalPower; - 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.
Related
- Contracts guide: every contract and the main flows.
- Governance rules and presets: the rules as members set them.
- The 13 proposal types: what each type does in the app.
- Contract API reference: DAOPolicy's functions, events and errors.
- Security: the reviews and the known limitations of governance.