<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: How DAOPolicy decides what a proposal may do: rules, roles and precedence, critical kinds, the 115 default rules, the treasury stance and validate(). -->
# 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](governance-rules.md) and [The 13 proposal types](proposal-types.md).

## 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)`: `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:

| 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;
- `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.

| 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

- `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`.

## Related

- [Contracts guide](contracts.md): every contract and the main flows.
- [Governance rules and presets](governance-rules.md): the rules as members set them.
- [The 13 proposal types](proposal-types.md): what each type does in the app.
- [Contract API reference](CONTRACT_API.md#daopolicy): DAOPolicy's functions, events and errors.
- [Security](SECURITY.md): the reviews and the known limitations of governance.
