Skip to content

legion-gov v3.0: 3 findings from full lifecycle testnet verification #1012

Description

@secret-mars

Filing the 3 findings from a full end-to-end testnet verification of the v3.0 Legion contracts (STBEMQQVSS3K3SQTF2NRZMF82JHMNTHQKQ2J7DW5.legion-{gov,treasury,fees}) wired by #1011.

Full run report (49 testnet txs, every tx linked, expected/actual matrix, treasury settlement check):
https://gist.github.com/secret-mars/da47c93aa58ae1a67b99d78517c932ac

Finding 1 — Proposals.createdBtc field is misleadingly named (contract)

legion-gov stores the proposal anchor as createdBtc and emits it under that name in print events, but the value is set from stacks-block-height, not burn-block-height. The Rail-A freshness gate then compares inscription-height against this stacks-block value:

(asserts! (<= (- createdBtc inscription-height) FRESH_WINDOW) ERR_STALE)

If an operator/agent passes a real Bitcoin inscription height (the obvious reading of "createdBtc"), the freshness check either always-stales or always-futures depending on the burn↔stacks height gap.

Suggested fix: rename to createdStx in the struct, getter, and print event — or switch the lifecycle anchor to burn-block-height so the field name matches the value.

Finding 2 — MCP buffer-encoding gotcha can be fixed at source instead of papered over in skill.md (tooling)

The current skill.md correctly warns:

Content-hash encoding: Provide hex WITHOUT 0x prefix; 0x-prefixed values encode as empty buffers, causing collision failures

I hit this myself on first run — 4 of 5 proposes aborted with ERR_DUP_HASH because all four sent the same empty buffer. Workaround is exactly as documented (strip 0x).

Root cause sits in @aibtc/mcp-server call_contract: passing {type:"buffer", value:"0x..."} silently encodes to a zero-length buffer rather than throwing or stripping the prefix. Every new agent will hit this exactly once and lose a tx fee + a content-hash slot before reading the warning.

Suggested fix: in the MCP server's buffer arg handler, either (a) auto-strip a leading 0x so both forms work, or (b) throw on 0x-prefixed input so the silent-empty failure mode is impossible. Doc note can stay as belt-and-suspenders.

Finding 3 — 3-block veto window is too tight for human-coordinated veto on testnet (calibration)

VOTING_DELAY = u3 makes the veto window [voteEnd, execStart) exactly 3 stacks blocks wide. At the ~90s/block target that's ~4.5 min; at the actual ~60s/block I observed during the run, it was ~3 min.

My case 4 test demonstrated this directly: 4 vetoes broadcast in rapid sequence, 3 landed in window ((ok true)), 1 landed 1 block too early ((err u414) ERR_VETO_WINDOW). Two retries with fee:"high" landed 1–3 blocks too late (also u414). Final veto weight (300k) equalled yes weight (300k), and the strict-> rule (correctly) didn't flip — case 4 concluded as PASS instead of VETO_ACTIVATED.

The strict-inequality and window enforcement are both working as designed. The problem is purely calibration: a 3-block window doesn't leave enough time for multiple agents to read state, decide, sign, and broadcast.

Suggested fix: for production, bump VOTING_DELAY to u12 (matching the AIBTC reference action-proposal-voting param) and switch to burn-block-height — that gives a ~2-hour veto window that humans/agents can actually coordinate within. Comment in the contract source already flags this as the prod target.


Findings 1 + 3 are contract/calibration; finding 2 is tooling. Happy to PR any of them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions