A reusable GenLayer Intelligent Contract that acts as a pre-governance
filter for DAOs. A DAO registers a small set of its own rules (each with
a stable ID and a plain-English description). Anyone can submit a
proposal; the contract uses GenLayer's AI-validator consensus to check
the proposal against every rule and records APPROVED or REJECTED
before the proposal is allowed into the DAO's normal governance process.
This contract does not vote, hold funds, manage tokens, track reputation, or replace DAO governance. Its only job is to answer one closed question: does a proposal satisfy the DAO's own predefined minimum requirements?
dao-proposal-guard/
contracts/dao_proposal_guard.py # the Intelligent Contract
tests/direct/test_dao_proposal_guard.py # 18 tests, run for real
tests/direct/conftest.py # pins the GenVM SDK version
gltest.config.yaml
- The DAO calls
create_dao_rules(rules_json)once, with a JSON array of{"id": ..., "description": ...}objects. - Anyone calls
submit_proposal(title, description, additional_information), which stores the proposal asPENDINGand returns its id. - Anyone calls
evaluate_proposal(proposal_id). A leader validator asks an LLM which of the DAO's rules (if any) the proposal fails to satisfy; other validators independently repeat the check; once they agree, the proposal's status is set toAPPROVEDorREJECTED. get_proposal_result(proposal_id)/get_all_proposals()/get_dao_rules()read the stored state.
The original spec's prompt asked the LLM to return both approved and
failed_rules:
{ "approved": boolean, "failed_rules": ["rule_id"] }This contract only ever asks for failed_rules. approved is
derived deterministically as len(failed_rules) == 0 and is never
requested from or trusted from the LLM.
This isn't a stylistic preference — it's a direct, hard-won lesson from
the other two contracts in this series. Both an AI URL Reputation Oracle
contract (safe next to a derived risk_tier) and a smart-contract
auditor contract (risk_level next to a derived risk_score) originally
let the LLM report a boolean/label alongside a value it should have
been consistent with, and both needed a follow-up fix after review found
that leader and validator could independently agree on a
self-contradictory pair — e.g. approved: true with a non-empty
failed_rules list, or approved: false with an empty one. GenLayer's
exact-match consensus only checks that two responses agree with each
other; it has no way to catch that a single response is internally
nonsensical. The AI URL Reputation Oracle project needed two separate
rounds of fixes before the equivalent field was fully closed in both
directions. Not requesting the redundant field at all avoids that
failure mode from the start instead of patching it in after a review
catches it.
Two smaller deviations, both defensive and both verified by a test:
create_dao_rulesis restricted to the contract's deployer (owner, captured viagl.message.sender_addressin__init__) and can only be called once (rules are immutable after that). The spec didn't specify either. Without access control, anyone could rewrite the rules a DAO is gating its own proposals with; without immutability, changing rules after proposals were already evaluated against the old ones would make stored results silently stale. Both are simple, low-cost guards consistent with the spec's own "minimal storage, low risk" goals — the trade-off is that this first version doesn't support updating a DAO's charter after the fact.create_dao_rulestakes a single JSON-encoded string parameter (rules_json) instead of arules[]-shaped parameter. Passing alist[...]directly as a write-method parameter (as opposed to a return type, which is confirmed to work) isn't demonstrated anywhere in the GenLayer documentation available while building this, so rather than guess at an unconfirmed API shape, rule registration reuses the same "encode as a JSON string, parse deterministically" pattern already proven to work for list-shaped data elsewhere in this series.
- The leader asks the LLM which rules (if any) the proposal fails.
- Each validator independently repeats the same check and its
failed_ruleslist — sorted and deduplicated on both sides before comparing — must exactly match the leader's. Two independently- correct responses that list the same failed rules in a different order, or with an accidental duplicate, are not treated as a disagreement. - Every rule ID an LLM lists in
failed_rulesis validated against the DAO's actually-registered rule IDs; an invented ID is treated as a malformed response ([LLM_ERROR], forces a leader rotation) rather than silently accepted or dropped. approvedis not compared separately in consensus — oncefailed_rulesmatches,approvedis guaranteed to match too, since it's computed by the identical formula on both sides.- Errors are classified with deterministic prefixes (
[EXPECTED],[EXTERNAL],[TRANSIENT],[LLM_ERROR]), matching the pattern used throughout this series.
The prompt itself follows the original spec's own instinct closely: it explicitly tells the LLM its task is not to judge whether the proposal is good, valuable, or likely to succeed — only whether it mechanically satisfies each rule. That instinct (avoid open-ended/subjective questions that give different LLMs room to reach different conclusions) is exactly right and is the reason this contract's consensus is much more likely to agree in practice than a contract asking for a free-form judgment or a numeric score would be.
| Check | Result |
|---|---|
genvm-lint check contracts/dao_proposal_guard.py |
Passed, 3/3, run for real |
genvm-lint typecheck contracts/dao_proposal_guard.py (Pyright + GenLayer SDK) |
Passed, 0 errors |
genvm-lint schema contracts/dao_proposal_guard.py |
6 methods (3 view, 3 write), matches the contract |
pytest tests/direct/ -v (real GenVM runtime, not mocked at the SDK level) |
18/18 passed |
Tests cover: owner-only/immutable rule configuration, the full
submit → evaluate → APPROVED/REJECTED flow, the approved-is-derived
regression test (a raw LLM response with a contradictory approved: true
- non-empty
failed_rulesis verified to still resolve toREJECTED), order/duplicate normalization reaching consensus, genuine disagreement being rejected, unknown rule IDs, missing/malformed LLM fields, and proposal lifecycle guards (not found, already evaluated, rules not yet configured).
tests/direct/conftest.py pins the GenVM SDK to v0.2.16 for the same
reason as the other projects in this series: genlayer-test's "latest"
auto-resolution currently points at a GenVM release (v0.3.0-rc7) whose
asset naming no longer matches what the installed genlayer-test
package's downloader expects, and this contract is written against the
pre-v0.3.0 API anyway (matching its Depends header), so v0.2.16 is the
correct, compatible pin.
gltest.config.yaml intentionally omits testnet_asimov/
testnet_bradbury by default — this file is validated as a whole on
every pytest/gltest run, even for the fast, in-memory tests/direct/
suite that needs no real credentials, so a network block referencing
unset ACCOUNT_PRIVATE_KEY_* env vars would fail every test run before a
single test executes. Add those blocks back (with real keys in a local,
uncommitted .env) only once you need to deploy to one of those
testnets.
pip install genvm-linter genlayer-test
genvm-lint check contracts/dao_proposal_guard.py --json
pytest tests/direct/ -vTokens, staking, voting, treasury, reputation, or any user/economy system — this is a single-purpose compliance filter, not a DAO framework. Also out of scope for this first version: mutable/versioned rule sets, file/URL attachments in proposals, and multi-DAO support (this contract instance represents one DAO's charter; deploy a fresh instance per DAO).