Skip to content

About

DAOProposalGuard is a reusable GenLayer Intelligent Contract that filters DAO proposals before they enter governance. A DAO registers its own rules once (each with a stable ID and description); anyone can submit a proposal, and GenLayer validators independently check it against every rule via LLM consensus before recording APPROVED or REJECTED.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

DAOProposalGuard

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

How it works

  1. The DAO calls create_dao_rules(rules_json) once, with a JSON array of {"id": ..., "description": ...} objects.
  2. Anyone calls submit_proposal(title, description, additional_information), which stores the proposal as PENDING and returns its id.
  3. 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 to APPROVED or REJECTED.
  4. get_proposal_result(proposal_id) / get_all_proposals() / get_dao_rules() read the stored state.

Where this deviates from the original spec, and why

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_rules is restricted to the contract's deployer (owner, captured via gl.message.sender_address in __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_rules takes a single JSON-encoded string parameter (rules_json) instead of a rules[]-shaped parameter. Passing a list[...] 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.

Consensus design

  • The leader asks the LLM which rules (if any) the proposal fails.
  • Each validator independently repeats the same check and its failed_rules list — 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_rules is 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.
  • approved is not compared separately in consensus — once failed_rules matches, approved is 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.

Verified in this environment

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_rules is verified to still resolve to REJECTED), 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.

Setup

pip install genvm-linter genlayer-test
genvm-lint check contracts/dao_proposal_guard.py --json
pytest tests/direct/ -v

Not implemented (intentionally, per spec)

Tokens, 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).

About

DAOProposalGuard is a reusable GenLayer Intelligent Contract that filters DAO proposals before they enter governance. A DAO registers its own rules once (each with a stable ID and description); anyone can submit a proposal, and GenLayer validators independently check it against every rule via LLM consensus before recording APPROVED or REJECTED.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages