{"tx_id":"0x2a80079947853833cbab6c8eb3e1eaa34b94b0dbfccec5a3a22e421035a7b5ca","canonical":true,"contract_id":"STBEMQQVSS3K3SQTF2NRZMF82JHMNTHQKQ2J7DW5.legion-gov","block_height":4020134,"clarity_version":3,"source_code":";; legion-gov\n;; Proposals + STAKE-WEIGHTED voting for the AIBTC Legion.\n;;\n;; This contract mirrors the production AIBTC action-proposal-voting extension\n;; (aibtc-action-proposal-voting.clar) governance model, adapted to our simpler\n;; stake-weighted design:\n;;\n;;   - There is NO DAO governance token. A voter's weight is exactly the amount of\n;;     sBTC they have staked through this contract's `stake` function (which is\n;;     forwarded into legion-treasury via `deposit`).\n;;   - The full burn-block proposal lifecycle (delay -> vote -> exec) is matched.\n;;   - Quorum / threshold / veto math matches the reference exactly.\n;;\n;; Timing uses stacks-block-height (like the reference test-fast config), NOT\n;; burn-block-height.\n;;\n;; Funds are denominated in sBTC: `stake` forwards a `<sip010-trait>` token into\n;; the treasury and `conclude-proposal` forwards the same trait reference into the\n;; treasury's execute-transfer. The treasury validates the token principal.\n;;\n;; -------------------------------------------------------------------\n;; v3.0 / Phase-1 additions (Agent-News -> Legion payout, demand-gated design):\n;;   1. PreCheckEnforcer  - computable Rail-A gates revert at `propose()`:\n;;        freshness (inscription window), content-hash de-dup (PaidHash registry),\n;;        and a minimum disjoint-source count. Slop never reaches a vote.\n;;   2. BondLock          - each proposal earmarks a bond from the proposer's own\n;;        stake. `locked-of` sums ALL of a proposer's open bonds, so one stake can\n;;        no longer back unlimited concurrent proposals. Stake is time-locked\n;;        (StakeLockedUntil) past the exec + challenge window, so a proposer cannot\n;;        unstake-and-run mid-lifecycle.\n;;   3. ProposerExclusion - the proposer may not vote on their own proposal, and\n;;        the quorum / veto denominator is ELIGIBLE (non-proposer) stake, so a\n;;        whale proposer holding the majority cannot brick honest quorum.\n;; (Demand-gated bounty, challenge market and soulbound rep are later phases.)\n;; -------------------------------------------------------------------\n\n;; -------------------------------------------------------------------\n;; Traits\n;; -------------------------------------------------------------------\n(use-trait sip010-trait 'STTWD9SPRQVD3P733V89SV0P8RZRZNQADG034F0A.faktory-trait-v1.sip-010-trait)\n\n;; -------------------------------------------------------------------\n;; Errors\n;; -------------------------------------------------------------------\n(define-constant ERR_INELIGIBLE (err u401)) ;; zero-stake / ineligible voter or proposer\n(define-constant ERR_NO_PROPOSAL (err u404)) ;; no such proposal\n(define-constant ERR_DOUBLE_VOTE (err u405)) ;; principal already voted (same direction)\n(define-constant ERR_SELF_TARGET (err u407)) ;; recipient is gov/treasury itself\n(define-constant ERR_ZERO_SNAPSHOT (err u410)) ;; no stake exists at proposal creation\n(define-constant ERR_VOTE_TOO_SOON (err u411)) ;; before voteStart\n(define-constant ERR_VOTE_TOO_LATE (err u412)) ;; at/after voteEnd\n(define-constant ERR_ALREADY_CONCLUDED (err u413)) ;; proposal already concluded\n(define-constant ERR_VETO_WINDOW (err u414)) ;; not in [voteEnd, execStart)\n(define-constant ERR_ALREADY_VETOED (err u415)) ;; principal already vetoed\n(define-constant ERR_NOT_IN_EXEC_WINDOW (err u416)) ;; conclude outside [execStart, execEnd)\n(define-constant ERR_ZERO_AMOUNT (err u417)) ;; stake/propose/unstake amount must be > 0\n(define-constant ERR_EMPTY_DESC (err u418)) ;; proposal description must be non-empty\n;; -- Phase-1 Rail-A / bond / exclusion errors --\n(define-constant ERR_STALE (err u419)) ;; inscription older than the freshness window\n(define-constant ERR_DUP_HASH (err u420)) ;; content hash already claimed/paid\n(define-constant ERR_THIN_SOURCING (err u421)) ;; fewer than MIN_SOURCES sources\n(define-constant ERR_INSUFFICIENT_BOND (err u422)) ;; free stake cannot cover the proposal bond\n(define-constant ERR_SELF_VOTE (err u423)) ;; proposer voting on own proposal\n(define-constant ERR_STAKE_LOCKED (err u424)) ;; unstake before StakeLockedUntil\n(define-constant ERR_INSUFFICIENT_UNSTAKE (err u425)) ;; unstake more than free (unlocked) stake\n(define-constant ERR_FUTURE_INSCRIPTION (err u426)) ;; inscription height is in the future\n\n;; -------------------------------------------------------------------\n;; Config (matches the reference parameter values)\n;; -------------------------------------------------------------------\n(define-constant TREASURY .legion-treasury)\n(define-constant SELF (as-contract tx-sender))\n\n(define-constant VOTING_QUORUM u15) ;; 15% turnout (of ELIGIBLE staked) required\n(define-constant VOTING_THRESHOLD u66) ;; 66% of cast votes must be yes\n;; TEST TIMING: short windows + Stacks-block (not burn-block) counting. Tuned so\n;; a full lifecycle (DELAY + PERIOD + DELAY + PERIOD = 96 stacks blocks) runs in\n;; ~1 hour on testnet (~3x the prior 32-block / ~20-min cadence). For production,\n;; revert to burn-block-height with VOTING_DELAY u12 / VOTING_PERIOD u24 (AIBTC-matched).\n(define-constant VOTING_DELAY u3) ;; stacks blocks between creation and vote start\n(define-constant VOTING_PERIOD u45) ;; stacks-block voting window length\n\n;; Our extra guard on top of the AIBTC model: require at least this many distinct\n;; voters before a proposal can execute.\n(define-constant MIN_PARTICIPANTS u2)\n\n;; -- Phase-1 Rail-A / bond config --\n;; Rail A: a story's inscription must be no older than this many stacks blocks at\n;; propose() time (combined A1/A2 freshness + A3 propose-window). ~144 blocks ~= 1 day.\n(define-constant FRESH_WINDOW u144)\n;; Rail A: minimum disjoint-domain source count (count-only gate; C1).\n(define-constant MIN_SOURCES u2)\n;; Bond earmarked from the proposer's stake, in basis points of the requested amount.\n(define-constant BOND_BPS u2000) ;; 20%\n;; Stake stays locked this many blocks past execEnd (placeholder for the Phase-2\n;; Rail-B challenge window). Prevents unstake-and-run before a proposal settles.\n(define-constant CHALLENGE_PERIOD u15)\n\n;; -------------------------------------------------------------------\n;; Data\n;; -------------------------------------------------------------------\n;; Stake per principal = voting weight.\n(define-map Stakes\n  principal\n  uint\n)\n\n;; Running total of all staked sBTC. Used as the basis for the per-proposal\n;; eligible-stake snapshot (denominator) and decremented on unstake.\n(define-data-var TotalStaked uint u0)\n\n;; BondLock: running sum of a principal's OPEN (unreleased) proposal bonds.\n;; `locked-of` reads this in O(1). It is the \"sum all open bonds\" the design\n;; requires so one stake cannot back unlimited concurrent proposals.\n(define-map LockedStake principal uint)\n\n;; BondLock: earliest stacks-block at which a principal may unstake. Set on\n;; propose to execEnd + CHALLENGE_PERIOD (kept monotonic across proposals).\n(define-map StakeLockedUntil principal uint)\n\n;; Per-proposal bond record (earmarked from the proposer's stake).\n(define-map ProposalBond\n  uint\n  {\n    proposer: principal,\n    locked: uint,\n    released: bool,\n  }\n)\n\n;; PreCheckEnforcer: on-chain registry of claimed content hashes. A hash is\n;; claimed at propose() and kept permanently if the proposal pays out; it is\n;; freed again if the proposal concludes as FAILED (so the story can be re-filed).\n(define-map PaidHash\n  (buff 32)\n  uint\n)\n\n(define-data-var ProposalNonce uint u0)\n\n(define-map Proposals\n  uint\n  {\n    proposer: principal,\n    desc: (string-ascii 256),\n    recipient: principal,\n    amount: uint,\n    ;; burn-block lifecycle anchor\n    createdBtc: uint,\n    ;; DEVIATION FROM REFERENCE:\n    ;; The reference snapshots each VOTER's token balance at the proposal's\n    ;; creation Stacks block via `at-block`, and uses liquid token supply as the\n    ;; quorum denominator. We instead snapshot the TOTAL staked amount into the\n    ;; proposal at creation time (`totalStakedSnapshot`) and derive the quorum /\n    ;; veto denominator as ELIGIBLE stake = snapshot - proposer's own stake\n    ;; (`eligibleSnapshot`). Per-voter weight is read as CURRENT stake at vote\n    ;; time. This is safe because staked sBTC is locked inside legion-treasury and\n    ;; cannot be withdrawn while a proposal is live (StakeLockedUntil), so\n    ;; mid-proposal vote-buying is not a cheap attack.\n    totalStakedSnapshot: uint,\n    proposerStake: uint,\n    eligibleSnapshot: uint,\n    bond: uint,\n    ;; Rail-A precheck inputs, recorded for auditability.\n    contentHash: (buff 32),\n    inscriptionHeight: uint,\n    sources: uint,\n    yesWeight: uint,\n    noWeight: uint,\n    vetoWeight: uint,\n    voterCount: uint,\n    concluded: bool,\n    executed: bool,\n  }\n)\n\n;; one vote RECORD per principal per proposal: stores their chosen direction and\n;; the weighted amount applied, so a voter can change their vote in-window.\n(define-map Votes\n  {\n    proposalId: uint,\n    voter: principal,\n  }\n  {\n    vote: bool,\n    amount: uint,\n  }\n)\n\n;; one veto per principal per proposal\n(define-map Vetoes\n  {\n    proposalId: uint,\n    voter: principal,\n  }\n  uint\n)\n\n;; -------------------------------------------------------------------\n;; Read-only views\n;; -------------------------------------------------------------------\n(define-read-only (get-stake (who principal))\n  (default-to u0 (map-get? Stakes who))\n)\n\n(define-read-only (get-total-staked)\n  (var-get TotalStaked)\n)\n\n;; BondLock: total of `who`'s open (unreleased) proposal bonds.\n(define-read-only (locked-of (who principal))\n  (default-to u0 (map-get? LockedStake who))\n)\n\n;; BondLock: earliest block `who` may unstake (u0 = never proposed / unlocked).\n(define-read-only (get-locked-until (who principal))\n  (default-to u0 (map-get? StakeLockedUntil who))\n)\n\n;; Free (unlocked, unbonded) stake `who` could unstake right now, ignoring time.\n(define-read-only (get-free-stake (who principal))\n  (- (get-stake who) (locked-of who))\n)\n\n(define-read-only (get-bond (id uint))\n  (map-get? ProposalBond id)\n)\n\n(define-read-only (is-hash-claimed (h (buff 32)))\n  (is-some (map-get? PaidHash h))\n)\n\n(define-read-only (get-proposal (id uint))\n  (map-get? Proposals id)\n)\n\n(define-read-only (get-proposal-count)\n  (var-get ProposalNonce)\n)\n\n(define-read-only (has-voted\n    (id uint)\n    (who principal)\n  )\n  (is-some (map-get? Votes {\n    proposalId: id,\n    voter: who,\n  }))\n)\n\n(define-read-only (get-vote-record\n    (id uint)\n    (who principal)\n  )\n  (map-get? Votes {\n    proposalId: id,\n    voter: who,\n  })\n)\n\n;; Mirrors the reference getter: returns computed lifecycle windows plus the\n;; current met-quorum / met-threshold / veto evaluation for a proposal. Quorum is\n;; measured against ELIGIBLE (non-proposer) stake.\n(define-read-only (get-proposal-status (id uint))\n  (match (map-get? Proposals id)\n    prop (let (\n        (createdBtc (get createdBtc prop))\n        (voteStart (+ createdBtc VOTING_DELAY))\n        (voteEnd (+ voteStart VOTING_PERIOD))\n        (execStart (+ voteEnd VOTING_DELAY))\n        (execEnd (+ execStart VOTING_PERIOD))\n        (yesWeight (get yesWeight prop))\n        (noWeight (get noWeight prop))\n        (vetoWeight (get vetoWeight prop))\n        (eligible (get eligibleSnapshot prop))\n        (castTotal (+ yesWeight noWeight))\n        (hasVotes (> castTotal u0))\n        (metQuorum (and\n          (> eligible u0)\n          hasVotes\n          (>= (/ (* castTotal u100) eligible) VOTING_QUORUM)\n        ))\n        (metThreshold (and\n          hasVotes\n          (>= (/ (* yesWeight u100) castTotal) VOTING_THRESHOLD)\n        ))\n        (vetoMetQuorum (and\n          (> eligible u0)\n          (> vetoWeight u0)\n          (>= (/ (* vetoWeight u100) eligible) VOTING_QUORUM)\n        ))\n        (vetoActivated (and vetoMetQuorum (> vetoWeight yesWeight)))\n      )\n      (some {\n        createdBtc: createdBtc,\n        voteStart: voteStart,\n        voteEnd: voteEnd,\n        execStart: execStart,\n        execEnd: execEnd,\n        yesWeight: yesWeight,\n        noWeight: noWeight,\n        vetoWeight: vetoWeight,\n        totalStakedSnapshot: (get totalStakedSnapshot prop),\n        eligibleSnapshot: eligible,\n        bond: (get bond prop),\n        voterCount: (get voterCount prop),\n        metQuorum: metQuorum,\n        metThreshold: metThreshold,\n        vetoMetQuorum: vetoMetQuorum,\n        vetoActivated: vetoActivated,\n        concluded: (get concluded prop),\n        executed: (get executed prop),\n      })\n    )\n    none\n  )\n)\n\n;; -------------------------------------------------------------------\n;; Public: stake\n;; -------------------------------------------------------------------\n;; Forwards `amount` sBTC into the treasury and credits the caller's voting\n;; weight. tx-sender is preserved into the treasury call, so the staker is the\n;; one debited. The `ft` trait reference is forwarded to the treasury, which\n;; asserts it matches the wired sBTC token.\n(define-public (stake\n    (ft <sip010-trait>)\n    (amount uint)\n  )\n  (begin\n    ;; Reject zero-value stakes up front (defense in depth; treasury also checks).\n    (asserts! (> amount u0) ERR_ZERO_AMOUNT)\n    ;; treasury.deposit asserts amount > u0, validates the token, and transfers.\n    (try! (contract-call? .legion-treasury deposit ft amount))\n    (map-set Stakes tx-sender (+ (get-stake tx-sender) amount))\n    (var-set TotalStaked (+ (var-get TotalStaked) amount))\n    (print {\n      event: \"stake\",\n      staker: tx-sender,\n      amount: amount,\n      total: (get-stake tx-sender),\n      totalStaked: (var-get TotalStaked),\n    })\n    (ok true)\n  )\n)\n\n;; -------------------------------------------------------------------\n;; Public: unstake\n;; -------------------------------------------------------------------\n;; Returns `amount` sBTC from the treasury to the caller. Only FREE stake (stake\n;; minus open bonds) is withdrawable, and only after StakeLockedUntil. gov is the\n;; authorized mover, so it routes the payout through treasury.execute-transfer.\n(define-public (unstake\n    (ft <sip010-trait>)\n    (amount uint)\n  )\n  (let (\n      (staker tx-sender)\n      (bal (get-stake tx-sender))\n      (locked (locked-of tx-sender))\n      (until (get-locked-until tx-sender))\n    )\n    (asserts! (> amount u0) ERR_ZERO_AMOUNT)\n    (asserts! (>= stacks-block-height until) ERR_STAKE_LOCKED)\n    ;; cannot withdraw stake that is earmarked as an open proposal bond\n    (asserts! (<= amount (- bal locked)) ERR_INSUFFICIENT_UNSTAKE)\n    ;; EFFECTS BEFORE INTERACTION\n    (map-set Stakes staker (- bal amount))\n    (var-set TotalStaked (- (var-get TotalStaked) amount))\n    (try! (contract-call? .legion-treasury execute-transfer ft staker amount))\n    (print {\n      event: \"unstake\",\n      staker: staker,\n      amount: amount,\n      remaining: (- bal amount),\n      totalStaked: (var-get TotalStaked),\n    })\n    (ok true)\n  )\n)\n\n;; -------------------------------------------------------------------\n;; Public: propose\n;; -------------------------------------------------------------------\n;; Snapshots the eligible staked weight, anchors the burn-block lifecycle, runs\n;; the Rail-A precheck (freshness / hash de-dup / sourcing), and earmarks a bond\n;; from the proposer's own stake.\n(define-public (propose\n    (desc (string-ascii 256))\n    (recipient principal)\n    (amount uint)\n    (content-hash (buff 32))\n    (inscription-height uint)\n    (sources uint)\n  )\n  (let (\n      (id (+ (var-get ProposalNonce) u1))\n      (snapshot (var-get TotalStaked))\n      (proposer-stake (get-stake tx-sender))\n      (already-locked (locked-of tx-sender))\n      (createdBtc stacks-block-height)\n      (bond (/ (* amount BOND_BPS) u10000))\n      (execEnd (+ createdBtc (+ (* u2 VOTING_DELAY) (* u2 VOTING_PERIOD))))\n      (lock-until (+ execEnd CHALLENGE_PERIOD))\n      (cur-lock (get-locked-until tx-sender))\n    )\n    ;; No self-targeting: cannot route funds to gov or treasury contracts.\n    (asserts! (and (not (is-eq recipient SELF)) (not (is-eq recipient TREASURY)))\n      ERR_SELF_TARGET\n    )\n    ;; A meaningful quorum denominator must exist.\n    (asserts! (> snapshot u0) ERR_ZERO_SNAPSHOT)\n    ;; Proposed payout must be a positive amount.\n    (asserts! (> amount u0) ERR_ZERO_AMOUNT)\n    ;; Reject blank descriptions; this also sanitizes `desc` before it is stored.\n    (asserts! (> (len desc) u0) ERR_EMPTY_DESC)\n    ;; BondLock: only a staker may propose, and free stake must cover the bond.\n    (asserts! (> proposer-stake u0) ERR_INELIGIBLE)\n    (asserts! (>= proposer-stake (+ already-locked bond)) ERR_INSUFFICIENT_BOND)\n    ;; -- Rail-A PreCheckEnforcer (computable gates revert here) --\n    ;; A2: the inscription cannot be in the future.\n    (asserts! (<= inscription-height createdBtc) ERR_FUTURE_INSCRIPTION)\n    ;; A1/A3: the inscription must be fresh (within the propose window).\n    (asserts! (<= (- createdBtc inscription-height) FRESH_WINDOW) ERR_STALE)\n    ;; B1: the content hash must not already be claimed/paid.\n    (asserts! (is-none (map-get? PaidHash content-hash)) ERR_DUP_HASH)\n    ;; C1: minimum disjoint-source count (count-only).\n    (asserts! (>= sources MIN_SOURCES) ERR_THIN_SOURCING)\n    ;; Claim the hash (freed again iff this proposal later fails).\n    (map-set PaidHash content-hash id)\n    ;; BondLock: earmark the bond and time-lock the proposer's stake.\n    (map-set LockedStake tx-sender (+ already-locked bond))\n    (map-set ProposalBond id {\n      proposer: tx-sender,\n      locked: bond,\n      released: false,\n    })\n    (map-set StakeLockedUntil tx-sender (if (> lock-until cur-lock)\n      lock-until\n      cur-lock\n    ))\n    (map-set Proposals id {\n      proposer: tx-sender,\n      desc: desc,\n      recipient: recipient,\n      amount: amount,\n      createdBtc: createdBtc,\n      totalStakedSnapshot: snapshot,\n      proposerStake: proposer-stake,\n      eligibleSnapshot: (- snapshot proposer-stake),\n      bond: bond,\n      contentHash: content-hash,\n      inscriptionHeight: inscription-height,\n      sources: sources,\n      yesWeight: u0,\n      noWeight: u0,\n      vetoWeight: u0,\n      voterCount: u0,\n      concluded: false,\n      executed: false,\n    })\n    (var-set ProposalNonce id)\n    (print {\n      event: \"propose\",\n      id: id,\n      proposer: tx-sender,\n      recipient: recipient,\n      amount: amount,\n      bond: bond,\n      contentHash: content-hash,\n      inscriptionHeight: inscription-height,\n      sources: sources,\n      createdBtc: createdBtc,\n      voteStart: (+ createdBtc VOTING_DELAY),\n      voteEnd: (+ (+ createdBtc VOTING_DELAY) VOTING_PERIOD),\n      totalStakedSnapshot: snapshot,\n      eligibleSnapshot: (- snapshot proposer-stake),\n    })\n    (ok id)\n  )\n)\n\n;; -------------------------------------------------------------------\n;; Public: vote\n;; -------------------------------------------------------------------\n;; Allowed only in [voteStart, voteEnd). The proposer may NOT vote on their own\n;; proposal. A voter may change their vote within the window: the previous\n;; weighted vote is subtracted and the new one added.\n(define-public (vote\n    (proposal-id uint)\n    (support bool)\n  )\n  (let (\n      (prop (unwrap! (map-get? Proposals proposal-id) ERR_NO_PROPOSAL))\n      (createdBtc (get createdBtc prop))\n      (voteStart (+ createdBtc VOTING_DELAY))\n      (voteEnd (+ voteStart VOTING_PERIOD))\n      (weight (get-stake tx-sender))\n      (prior (map-get? Votes {\n        proposalId: proposal-id,\n        voter: tx-sender,\n      }))\n    )\n    ;; ProposerExclusion: the proposer cannot vote on their own proposal.\n    (asserts! (not (is-eq tx-sender (get proposer prop))) ERR_SELF_VOTE)\n    ;; voter must hold non-zero stake\n    (asserts! (> weight u0) ERR_INELIGIBLE)\n    ;; proposal not concluded\n    (asserts! (not (get concluded prop)) ERR_ALREADY_CONCLUDED)\n    ;; voting window: [voteStart, voteEnd)\n    (asserts! (>= stacks-block-height voteStart) ERR_VOTE_TOO_SOON)\n    (asserts! (< stacks-block-height voteEnd) ERR_VOTE_TOO_LATE)\n    ;; if a prior vote exists, the new vote must be a CHANGE of direction\n    (and\n      (is-some prior)\n      (asserts! (not (is-eq (get vote (unwrap-panic prior)) support))\n        ERR_DOUBLE_VOTE\n      )\n    )\n    ;; remove the previous weighted vote (if any)\n    (let (\n        (priorYes (match prior\n          r (if (get vote r)\n            (get amount r)\n            u0\n          )\n          u0\n        ))\n        (priorNo (match prior\n          r (if (get vote r)\n            u0\n            (get amount r)\n          )\n          u0\n        ))\n        (baseYes (- (get yesWeight prop) priorYes))\n        (baseNo (- (get noWeight prop) priorNo))\n        (newVoterCount (if (is-some prior)\n          (get voterCount prop)\n          (+ (get voterCount prop) u1)\n        ))\n      )\n      (map-set Votes {\n        proposalId: proposal-id,\n        voter: tx-sender,\n      } {\n        vote: support,\n        amount: weight,\n      })\n      (map-set Proposals proposal-id\n        (merge prop {\n          yesWeight: (if support\n            (+ baseYes weight)\n            baseYes\n          ),\n          noWeight: (if support\n            baseNo\n            (+ baseNo weight)\n          ),\n          voterCount: newVoterCount,\n        })\n      )\n      (print {\n        event: \"vote\",\n        id: proposal-id,\n        voter: tx-sender,\n        support: support,\n        weight: weight,\n        changed: (is-some prior),\n      })\n      (ok true)\n    )\n  )\n)\n\n;; -------------------------------------------------------------------\n;; Public: veto\n;; -------------------------------------------------------------------\n;; Callable in [voteEnd, execStart) by any staker. One veto per principal.\n(define-public (veto (proposal-id uint))\n  (let (\n      (prop (unwrap! (map-get? Proposals proposal-id) ERR_NO_PROPOSAL))\n      (createdBtc (get createdBtc prop))\n      (voteEnd (+ (+ createdBtc VOTING_DELAY) VOTING_PERIOD))\n      (execStart (+ voteEnd VOTING_DELAY))\n      (weight (get-stake tx-sender))\n    )\n    (asserts! (> weight u0) ERR_INELIGIBLE)\n    (asserts! (not (get concluded prop)) ERR_ALREADY_CONCLUDED)\n    ;; veto window: [voteEnd, execStart)\n    (asserts! (>= stacks-block-height voteEnd) ERR_VETO_WINDOW)\n    (asserts! (< stacks-block-height execStart) ERR_VETO_WINDOW)\n    (asserts!\n      (is-none (map-get? Vetoes {\n        proposalId: proposal-id,\n        voter: tx-sender,\n      }))\n      ERR_ALREADY_VETOED\n    )\n    (map-set Vetoes {\n      proposalId: proposal-id,\n      voter: tx-sender,\n    }\n      weight\n    )\n    (map-set Proposals proposal-id\n      (merge prop { vetoWeight: (+ (get vetoWeight prop) weight) })\n    )\n    (print {\n      event: \"veto\",\n      id: proposal-id,\n      voter: tx-sender,\n      weight: weight,\n    })\n    (ok true)\n  )\n)\n\n;; -------------------------------------------------------------------\n;; Public: conclude-proposal\n;; -------------------------------------------------------------------\n;; Callable only in [execStart, execEnd) and only once. Marks the proposal\n;; concluded BEFORE any external call (effects-before-interaction). Releases the\n;; proposer's bond, and frees the content hash iff the proposal FAILED (so the\n;; story can be re-filed; a paid hash stays claimed forever). Executes the\n;; treasury transfer ONLY IF the proposal met (eligible) quorum AND threshold AND\n;; has >= MIN_PARTICIPANTS voters AND was not veto-activated.\n(define-public (conclude-proposal\n    (proposal-id uint)\n    (ft <sip010-trait>)\n  )\n  (let (\n      (prop (unwrap! (map-get? Proposals proposal-id) ERR_NO_PROPOSAL))\n      (bondInfo (unwrap! (map-get? ProposalBond proposal-id) ERR_NO_PROPOSAL))\n      (createdBtc (get createdBtc prop))\n      (voteEnd (+ (+ createdBtc VOTING_DELAY) VOTING_PERIOD))\n      (execStart (+ voteEnd VOTING_DELAY))\n      (execEnd (+ execStart VOTING_PERIOD))\n      (yesWeight (get yesWeight prop))\n      (noWeight (get noWeight prop))\n      (vetoWeight (get vetoWeight prop))\n      (eligible (get eligibleSnapshot prop))\n      (castTotal (+ yesWeight noWeight))\n      (hasVotes (> castTotal u0))\n      ;; quorum: total cast vs ELIGIBLE snapshot. Guard against zero / no votes.\n      (metQuorum (and\n        (> eligible u0)\n        hasVotes\n        (>= (/ (* castTotal u100) eligible) VOTING_QUORUM)\n      ))\n      ;; threshold: yes vs cast. Guard against div-by-zero (castTotal > 0).\n      (metThreshold (and\n        hasVotes\n        (>= (/ (* yesWeight u100) castTotal) VOTING_THRESHOLD)\n      ))\n      (vetoMetQuorum (and\n        (> eligible u0)\n        (> vetoWeight u0)\n        (>= (/ (* vetoWeight u100) eligible) VOTING_QUORUM)\n      ))\n      (vetoActivated (and vetoMetQuorum (> vetoWeight yesWeight)))\n      (enoughParticipants (>= (get voterCount prop) MIN_PARTICIPANTS))\n      (votePassed (and\n        metQuorum\n        metThreshold\n        enoughParticipants\n        (not vetoActivated)\n      ))\n      (proposer (get proposer prop))\n      (bondAmt (get locked bondInfo))\n    )\n    ;; not already concluded\n    (asserts! (not (get concluded prop)) ERR_ALREADY_CONCLUDED)\n    ;; execution window: [execStart, execEnd)\n    (asserts! (>= stacks-block-height execStart) ERR_NOT_IN_EXEC_WINDOW)\n    (asserts! (< stacks-block-height execEnd) ERR_NOT_IN_EXEC_WINDOW)\n    ;; EFFECTS BEFORE INTERACTION:\n    ;; release the proposer's bond back into free stake.\n    (map-set ProposalBond proposal-id (merge bondInfo { released: true }))\n    (map-set LockedStake proposer (- (locked-of proposer) bondAmt))\n    ;; free the content hash iff the proposal failed (paid hashes stay claimed).\n    (if votePassed\n      true\n      (map-delete PaidHash (get contentHash prop))\n    )\n    ;; mark concluded (and executed iff passing) before the external call.\n    (map-set Proposals proposal-id\n      (merge prop {\n        concluded: true,\n        executed: votePassed,\n      })\n    )\n    (print {\n      event: \"conclude-proposal\",\n      id: proposal-id,\n      recipient: (get recipient prop),\n      amount: (get amount prop),\n      yesWeight: yesWeight,\n      noWeight: noWeight,\n      vetoWeight: vetoWeight,\n      eligibleSnapshot: eligible,\n      metQuorum: metQuorum,\n      metThreshold: metThreshold,\n      vetoActivated: vetoActivated,\n      enoughParticipants: enoughParticipants,\n      bondReleased: bondAmt,\n      passed: votePassed,\n    })\n    (if votePassed\n      (try! (contract-call? .legion-treasury execute-transfer ft (get recipient prop)\n        (get amount prop)\n      ))\n      true\n    )\n    (ok votePassed)\n  )\n)\n","abi":"{\"maps\":[{\"key\":\"principal\",\"name\":\"LockedStake\",\"value\":\"uint128\"},{\"key\":{\"buffer\":{\"length\":32}},\"name\":\"PaidHash\",\"value\":\"uint128\"},{\"key\":\"uint128\",\"name\":\"ProposalBond\",\"value\":{\"tuple\":[{\"name\":\"locked\",\"type\":\"uint128\"},{\"name\":\"proposer\",\"type\":\"principal\"},{\"name\":\"released\",\"type\":\"bool\"}]}},{\"key\":\"uint128\",\"name\":\"Proposals\",\"value\":{\"tuple\":[{\"name\":\"amount\",\"type\":\"uint128\"},{\"name\":\"bond\",\"type\":\"uint128\"},{\"name\":\"concluded\",\"type\":\"bool\"},{\"name\":\"contentHash\",\"type\":{\"buffer\":{\"length\":32}}},{\"name\":\"createdBtc\",\"type\":\"uint128\"},{\"name\":\"desc\",\"type\":{\"string-ascii\":{\"length\":256}}},{\"name\":\"eligibleSnapshot\",\"type\":\"uint128\"},{\"name\":\"executed\",\"type\":\"bool\"},{\"name\":\"inscriptionHeight\",\"type\":\"uint128\"},{\"name\":\"noWeight\",\"type\":\"uint128\"},{\"name\":\"proposer\",\"type\":\"principal\"},{\"name\":\"proposerStake\",\"type\":\"uint128\"},{\"name\":\"recipient\",\"type\":\"principal\"},{\"name\":\"sources\",\"type\":\"uint128\"},{\"name\":\"totalStakedSnapshot\",\"type\":\"uint128\"},{\"name\":\"vetoWeight\",\"type\":\"uint128\"},{\"name\":\"voterCount\",\"type\":\"uint128\"},{\"name\":\"yesWeight\",\"type\":\"uint128\"}]}},{\"key\":\"principal\",\"name\":\"StakeLockedUntil\",\"value\":\"uint128\"},{\"key\":\"principal\",\"name\":\"Stakes\",\"value\":\"uint128\"},{\"key\":{\"tuple\":[{\"name\":\"proposalId\",\"type\":\"uint128\"},{\"name\":\"voter\",\"type\":\"principal\"}]},\"name\":\"Vetoes\",\"value\":\"uint128\"},{\"key\":{\"tuple\":[{\"name\":\"proposalId\",\"type\":\"uint128\"},{\"name\":\"voter\",\"type\":\"principal\"}]},\"name\":\"Votes\",\"value\":{\"tuple\":[{\"name\":\"amount\",\"type\":\"uint128\"},{\"name\":\"vote\",\"type\":\"bool\"}]}}],\"epoch\":\"Epoch34\",\"functions\":[{\"args\":[{\"name\":\"proposal-id\",\"type\":\"uint128\"},{\"name\":\"ft\",\"type\":\"trait_reference\"}],\"name\":\"conclude-proposal\",\"access\":\"public\",\"outputs\":{\"type\":{\"response\":{\"ok\":\"bool\",\"error\":\"uint128\"}}}},{\"args\":[{\"name\":\"desc\",\"type\":{\"string-ascii\":{\"length\":256}}},{\"name\":\"recipient\",\"type\":\"principal\"},{\"name\":\"amount\",\"type\":\"uint128\"},{\"name\":\"content-hash\",\"type\":{\"buffer\":{\"length\":32}}},{\"name\":\"inscription-height\",\"type\":\"uint128\"},{\"name\":\"sources\",\"type\":\"uint128\"}],\"name\":\"propose\",\"access\":\"public\",\"outputs\":{\"type\":{\"response\":{\"ok\":\"uint128\",\"error\":\"uint128\"}}}},{\"args\":[{\"name\":\"ft\",\"type\":\"trait_reference\"},{\"name\":\"amount\",\"type\":\"uint128\"}],\"name\":\"stake\",\"access\":\"public\",\"outputs\":{\"type\":{\"response\":{\"ok\":\"bool\",\"error\":\"uint128\"}}}},{\"args\":[{\"name\":\"ft\",\"type\":\"trait_reference\"},{\"name\":\"amount\",\"type\":\"uint128\"}],\"name\":\"unstake\",\"access\":\"public\",\"outputs\":{\"type\":{\"response\":{\"ok\":\"bool\",\"error\":\"uint128\"}}}},{\"args\":[{\"name\":\"proposal-id\",\"type\":\"uint128\"}],\"name\":\"veto\",\"access\":\"public\",\"outputs\":{\"type\":{\"response\":{\"ok\":\"bool\",\"error\":\"uint128\"}}}},{\"args\":[{\"name\":\"proposal-id\",\"type\":\"uint128\"},{\"name\":\"support\",\"type\":\"bool\"}],\"name\":\"vote\",\"access\":\"public\",\"outputs\":{\"type\":{\"response\":{\"ok\":\"bool\",\"error\":\"uint128\"}}}},{\"args\":[{\"name\":\"id\",\"type\":\"uint128\"}],\"name\":\"get-bond\",\"access\":\"read_only\",\"outputs\":{\"type\":{\"optional\":{\"tuple\":[{\"name\":\"locked\",\"type\":\"uint128\"},{\"name\":\"proposer\",\"type\":\"principal\"},{\"name\":\"released\",\"type\":\"bool\"}]}}}},{\"args\":[{\"name\":\"who\",\"type\":\"principal\"}],\"name\":\"get-free-stake\",\"access\":\"read_only\",\"outputs\":{\"type\":\"uint128\"}},{\"args\":[{\"name\":\"who\",\"type\":\"principal\"}],\"name\":\"get-locked-until\",\"access\":\"read_only\",\"outputs\":{\"type\":\"uint128\"}},{\"args\":[{\"name\":\"id\",\"type\":\"uint128\"}],\"name\":\"get-proposal\",\"access\":\"read_only\",\"outputs\":{\"type\":{\"optional\":{\"tuple\":[{\"name\":\"amount\",\"type\":\"uint128\"},{\"name\":\"bond\",\"type\":\"uint128\"},{\"name\":\"concluded\",\"type\":\"bool\"},{\"name\":\"contentHash\",\"type\":{\"buffer\":{\"length\":32}}},{\"name\":\"createdBtc\",\"type\":\"uint128\"},{\"name\":\"desc\",\"type\":{\"string-ascii\":{\"length\":256}}},{\"name\":\"eligibleSnapshot\",\"type\":\"uint128\"},{\"name\":\"executed\",\"type\":\"bool\"},{\"name\":\"inscriptionHeight\",\"type\":\"uint128\"},{\"name\":\"noWeight\",\"type\":\"uint128\"},{\"name\":\"proposer\",\"type\":\"principal\"},{\"name\":\"proposerStake\",\"type\":\"uint128\"},{\"name\":\"recipient\",\"type\":\"principal\"},{\"name\":\"sources\",\"type\":\"uint128\"},{\"name\":\"totalStakedSnapshot\",\"type\":\"uint128\"},{\"name\":\"vetoWeight\",\"type\":\"uint128\"},{\"name\":\"voterCount\",\"type\":\"uint128\"},{\"name\":\"yesWeight\",\"type\":\"uint128\"}]}}}},{\"args\":[],\"name\":\"get-proposal-count\",\"access\":\"read_only\",\"outputs\":{\"type\":\"uint128\"}},{\"args\":[{\"name\":\"id\",\"type\":\"uint128\"}],\"name\":\"get-proposal-status\",\"access\":\"read_only\",\"outputs\":{\"type\":{\"optional\":{\"tuple\":[{\"name\":\"bond\",\"type\":\"uint128\"},{\"name\":\"concluded\",\"type\":\"bool\"},{\"name\":\"createdBtc\",\"type\":\"uint128\"},{\"name\":\"eligibleSnapshot\",\"type\":\"uint128\"},{\"name\":\"execEnd\",\"type\":\"uint128\"},{\"name\":\"execStart\",\"type\":\"uint128\"},{\"name\":\"executed\",\"type\":\"bool\"},{\"name\":\"metQuorum\",\"type\":\"bool\"},{\"name\":\"metThreshold\",\"type\":\"bool\"},{\"name\":\"noWeight\",\"type\":\"uint128\"},{\"name\":\"totalStakedSnapshot\",\"type\":\"uint128\"},{\"name\":\"vetoActivated\",\"type\":\"bool\"},{\"name\":\"vetoMetQuorum\",\"type\":\"bool\"},{\"name\":\"vetoWeight\",\"type\":\"uint128\"},{\"name\":\"voteEnd\",\"type\":\"uint128\"},{\"name\":\"voteStart\",\"type\":\"uint128\"},{\"name\":\"voterCount\",\"type\":\"uint128\"},{\"name\":\"yesWeight\",\"type\":\"uint128\"}]}}}},{\"args\":[{\"name\":\"who\",\"type\":\"principal\"}],\"name\":\"get-stake\",\"access\":\"read_only\",\"outputs\":{\"type\":\"uint128\"}},{\"args\":[],\"name\":\"get-total-staked\",\"access\":\"read_only\",\"outputs\":{\"type\":\"uint128\"}},{\"args\":[{\"name\":\"id\",\"type\":\"uint128\"},{\"name\":\"who\",\"type\":\"principal\"}],\"name\":\"get-vote-record\",\"access\":\"read_only\",\"outputs\":{\"type\":{\"optional\":{\"tuple\":[{\"name\":\"amount\",\"type\":\"uint128\"},{\"name\":\"vote\",\"type\":\"bool\"}]}}}},{\"args\":[{\"name\":\"id\",\"type\":\"uint128\"},{\"name\":\"who\",\"type\":\"principal\"}],\"name\":\"has-voted\",\"access\":\"read_only\",\"outputs\":{\"type\":\"bool\"}},{\"args\":[{\"name\":\"h\",\"type\":{\"buffer\":{\"length\":32}}}],\"name\":\"is-hash-claimed\",\"access\":\"read_only\",\"outputs\":{\"type\":\"bool\"}},{\"args\":[{\"name\":\"who\",\"type\":\"principal\"}],\"name\":\"locked-of\",\"access\":\"read_only\",\"outputs\":{\"type\":\"uint128\"}}],\"variables\":[{\"name\":\"BOND_BPS\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"CHALLENGE_PERIOD\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"ERR_ALREADY_CONCLUDED\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_ALREADY_VETOED\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_DOUBLE_VOTE\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_DUP_HASH\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_EMPTY_DESC\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_FUTURE_INSCRIPTION\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_INELIGIBLE\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_INSUFFICIENT_BOND\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_INSUFFICIENT_UNSTAKE\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_NOT_IN_EXEC_WINDOW\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_NO_PROPOSAL\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_SELF_TARGET\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_SELF_VOTE\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_STAKE_LOCKED\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_STALE\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_THIN_SOURCING\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_VETO_WINDOW\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_VOTE_TOO_LATE\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_VOTE_TOO_SOON\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_ZERO_AMOUNT\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"ERR_ZERO_SNAPSHOT\",\"type\":{\"response\":{\"ok\":\"none\",\"error\":\"uint128\"}},\"access\":\"constant\"},{\"name\":\"FRESH_WINDOW\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"MIN_PARTICIPANTS\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"MIN_SOURCES\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"SELF\",\"type\":\"principal\",\"access\":\"constant\"},{\"name\":\"TREASURY\",\"type\":\"principal\",\"access\":\"constant\"},{\"name\":\"VOTING_DELAY\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"VOTING_PERIOD\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"VOTING_QUORUM\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"VOTING_THRESHOLD\",\"type\":\"uint128\",\"access\":\"constant\"},{\"name\":\"ProposalNonce\",\"type\":\"uint128\",\"access\":\"variable\"},{\"name\":\"TotalStaked\",\"type\":\"uint128\",\"access\":\"variable\"}],\"clarity_version\":\"Clarity3\",\"fungible_tokens\":[],\"non_fungible_tokens\":[]}"}