From e087e230a660c6a3aa79c12a9d1f04c9aba5c147 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 9 Aug 2024 15:09:47 -0700 Subject: [PATCH 1/9] fix: rebase from main --- Clarinet.toml | 6 + .../ccip024-miamicoin-signal-vote.clar | 263 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 contracts/proposals/ccip024-miamicoin-signal-vote.clar diff --git a/Clarinet.toml b/Clarinet.toml index 779a6a6..d1c404e 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -191,10 +191,16 @@ path = "contracts/proposals/ccip022-treasury-redemption-nyc.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip024-miamicoin-signal-vote] +path = "contracts/proposals/ccip024-miamicoin-signal-vote.clar" +clarity_version = 2 +epoch = 2.4 + [contracts.ccip019-pox-4-stacking] path = "contracts/proposals/ccip019-pox-4-stacking.clar" clarity_version = 2 epoch = 2.5 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] diff --git a/contracts/proposals/ccip024-miamicoin-signal-vote.clar b/contracts/proposals/ccip024-miamicoin-signal-vote.clar new file mode 100644 index 0000000..900289c --- /dev/null +++ b/contracts/proposals/ccip024-miamicoin-signal-vote.clar @@ -0,0 +1,263 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip015-trait.ccip015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u24000)) +(define-constant ERR_SAVING_VOTE (err u24001)) +(define-constant ERR_VOTED_ALREADY (err u24002)) +(define-constant ERR_NOTHING_STACKED (err u24003)) +(define-constant ERR_USER_NOT_FOUND (err u24004)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u24005)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u24006)) +(define-constant ERR_VOTE_FAILED (err u24007)) + +;; CONSTANTS + +(define-constant SELF (as-contract tx-sender)) +(define-constant CCIP_024 { + name: "MiamiCoin Community Signal Vote", + link: "https://github.com/citycoins/governance/blob/feat/add-ccip-024/ccips/ccip-024/ccip-024-miamicoin-community-signal-vote.md", + hash: "TBD", +}) + +(define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places +(define-constant VOTE_LENGTH u2016) ;; approximately 2 weeks in Bitcoin blocks + +;; set city ID +(define-constant MIA_ID (default-to u1 (contract-call? .ccd004-city-registry get-city-id "mia"))) + +;; DATA VARS + +;; vote block heights +(define-data-var voteActive bool true) +(define-data-var voteStart uint u0) +(define-data-var voteEnd uint u0) + +;; start the vote when deployed +(var-set voteStart block-height) +(var-set voteEnd (+ block-height VOTE_LENGTH)) + +;; DATA MAPS + +(define-map CityVotes + uint ;; city ID + { ;; vote + totalAmountYes: uint, + totalAmountNo: uint, + totalVotesYes: uint, + totalVotesNo: uint, + } +) + +(define-map UserVotes + uint ;; user ID + { ;; vote + vote: bool, + mia: uint, + } +) + +;; PUBLIC FUNCTIONS + +(define-public (execute (sender principal)) + (begin + ;; check vote is complete/passed + (try! (is-executable)) + ;; update vote variables + (var-set voteActive false) + ;; no action to execute, this is a signal vote + (ok true)) +) + +(define-public (vote-on-proposal (vote bool)) + (let + ( + (voterId (unwrap! (contract-call? .ccd003-user-registry get-user-id contract-caller) ERR_USER_NOT_FOUND)) + (voterRecord (map-get? UserVotes voterId)) + ) + ;; check if vote is active + (asserts! (var-get voteActive) ERR_PROPOSAL_NOT_ACTIVE) + ;; check if voting period has ended + (asserts! (<= block-height (var-get voteEnd)) ERR_PROPOSAL_NOT_ACTIVE) + ;; check if vote record exists for user + (match voterRecord record + ;; if the voterRecord exists + (let + ( + (oldVote (get vote record)) + (miaVoteAmount (get mia record)) + ) + ;; check vote is not the same as before + (asserts! (not (is-eq oldVote vote)) ERR_VOTED_ALREADY) + ;; record the new vote for the user + (map-set UserVotes voterId + (merge record { vote: vote }) + ) + ;; update vote stats for MIA + (update-city-votes MIA_ID miaVoteAmount vote true) + (ok true) + ) + ;; if the voterRecord does not exist + (let + ( + (miaVoteAmount (scale-down (default-to u0 (get-mia-vote voterId true)))) + ) + ;; check that the user has a positive vote + (asserts! (> miaVoteAmount u0) ERR_NOTHING_STACKED) + ;; insert new user vote record + (asserts! (map-insert UserVotes voterId { + vote: vote, + mia: miaVoteAmount + }) ERR_SAVING_VOTE) + ;; update vote stats for MIA + (update-city-votes MIA_ID miaVoteAmount vote false) + (ok true) + ) + ) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-executable) + (let + ( + (votingRecord (unwrap! (get-vote-totals) ERR_PANIC)) + (miaRecord (get mia votingRecord)) + (voteTotals (get totals votingRecord)) + ) + ;; check that there is at least one vote + (asserts! (or (> (get totalVotesYes voteTotals) u0) (> (get totalVotesNo voteTotals) u0)) ERR_VOTE_FAILED) + ;; check that the yes total is more than no total + (asserts! (> (get totalVotesYes voteTotals) (get totalVotesNo voteTotals)) ERR_VOTE_FAILED) + ;; check the "yes" votes are at least 25% of the total + (asserts! (>= (get totalAmountYes miaRecord) (/ (get totalAmountYes voteTotals) u4)) ERR_VOTE_FAILED) + ;; check that the voting period has ended + (asserts! (> block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) + ;; allow execution + (ok true) + ) +) + +(define-read-only (is-vote-active) + (some (var-get voteActive)) +) + +(define-read-only (get-proposal-info) + (some CCIP_024) +) + +(define-read-only (get-vote-period) + (some { + startBlock: (var-get voteStart), + endBlock: (var-get voteEnd), + length: VOTE_LENGTH + }) +) + +(define-read-only (get-vote-total-mia) + (map-get? CityVotes MIA_ID) +) + +(define-read-only (get-vote-total-mia-or-default) + (default-to { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } (get-vote-total-mia)) +) + +(define-read-only (get-vote-totals) + (let + ( + (miaRecord (get-vote-total-mia-or-default)) + ) + (some { + mia: miaRecord, + totals: { + totalAmountYes: (get totalAmountYes miaRecord), + totalAmountNo: (get totalAmountNo miaRecord), + totalVotesYes: (get totalVotesYes miaRecord), + totalVotesNo: (get totalVotesNo miaRecord), + } + }) + ) +) + +(define-read-only (get-voter-info (id uint)) + (map-get? UserVotes id) +) + +;; MIA vote calculation +;; returns (some uint) or (none) +;; optionally scaled by VOTE_SCALE_FACTOR (10^6) +(define-read-only (get-mia-vote (userId uint) (scaled bool)) + (let + ( + ;; MAINNET: MIA cycle 82 / first block BTC 838,250 STX 145,643 + ;; cycle 2 / u4500 used in tests + (cycle82Hash (unwrap! (get-block-hash u4500) none)) + (cycle82Data (at-block cycle82Hash (contract-call? .ccd007-citycoin-stacking get-stacker MIA_ID u2 userId))) + (cycle82Amount (get stacked cycle82Data)) + ;; MAINNET: MIA cycle 83 / first block BTC 840,350 STX 147,282 + ;; cycle 3 / u6600 used in tests + (cycle83Hash (unwrap! (get-block-hash u6600) none)) + (cycle83Data (at-block cycle83Hash (contract-call? .ccd007-citycoin-stacking get-stacker MIA_ID u3 userId))) + (cycle83Amount (get stacked cycle83Data)) + ;; MIA vote calculation + (scaledVote (/ (+ (scale-up cycle82Amount) (scale-up cycle83Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle82Amount u0) (> cycle83Amount u0)) none) + ;; return scaled or unscaled value + (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) + ) +) + +;; PRIVATE FUNCTIONS + +;; update city vote map +(define-private (update-city-votes (cityId uint) (voteAmount uint) (vote bool) (changedVote bool)) + (let + ( + (cityRecord (default-to + { totalAmountYes: u0, totalAmountNo: u0, totalVotesYes: u0, totalVotesNo: u0 } + (map-get? CityVotes cityId))) + ) + ;; do not record if amount is 0 + (if (> voteAmount u0) + ;; handle vote + (if vote + ;; handle yes vote + (map-set CityVotes cityId { + totalAmountYes: (+ voteAmount (get totalAmountYes cityRecord)), + totalVotesYes: (+ u1 (get totalVotesYes cityRecord)), + totalAmountNo: (if changedVote (- (get totalAmountNo cityRecord) voteAmount) (get totalAmountNo cityRecord)), + totalVotesNo: (if changedVote (- (get totalVotesNo cityRecord) u1) (get totalVotesNo cityRecord)) + }) + ;; handle no vote + (map-set CityVotes cityId { + totalAmountYes: (if changedVote (- (get totalAmountYes cityRecord) voteAmount) (get totalAmountYes cityRecord)), + totalVotesYes: (if changedVote (- (get totalVotesYes cityRecord) u1) (get totalVotesYes cityRecord)), + totalAmountNo: (+ voteAmount (get totalAmountNo cityRecord)), + totalVotesNo: (+ u1 (get totalVotesNo cityRecord)), + }) + ) + ;; ignore calls with vote amount equal to 0 + false) + ) +) + +;; get block hash by height +(define-private (get-block-hash (blockHeight uint)) + (get-block-info? id-header-hash blockHeight) +) + +;; CREDIT: ALEX math-fixed-point-16.clar + +(define-private (scale-up (a uint)) + (* a VOTE_SCALE_FACTOR) +) + +(define-private (scale-down (a uint)) + (/ a VOTE_SCALE_FACTOR) +) \ No newline at end of file From a4cfe7f06788e064f7b5fdeef2c76b0df8933fec Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 2 Aug 2024 15:03:23 -0700 Subject: [PATCH 2/9] fix: simplify structure and stub execution --- .../ccip024-miamicoin-signal-vote.clar | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/contracts/proposals/ccip024-miamicoin-signal-vote.clar b/contracts/proposals/ccip024-miamicoin-signal-vote.clar index 900289c..889ed36 100644 --- a/contracts/proposals/ccip024-miamicoin-signal-vote.clar +++ b/contracts/proposals/ccip024-miamicoin-signal-vote.clar @@ -33,12 +33,10 @@ ;; vote block heights (define-data-var voteActive bool true) -(define-data-var voteStart uint u0) -(define-data-var voteEnd uint u0) - ;; start the vote when deployed -(var-set voteStart block-height) -(var-set voteEnd (+ block-height VOTE_LENGTH)) +(define-data-var voteStart uint block-height) +;; end the vote after defined period +(define-data-var voteEnd uint (+ block-height VOTE_LENGTH)) ;; DATA MAPS @@ -63,13 +61,8 @@ ;; PUBLIC FUNCTIONS (define-public (execute (sender principal)) - (begin - ;; check vote is complete/passed - (try! (is-executable)) - ;; update vote variables - (var-set voteActive false) - ;; no action to execute, this is a signal vote - (ok true)) + ;; no action to execute, this is a signal vote + (ok true) ) (define-public (vote-on-proposal (vote bool)) @@ -123,23 +116,8 @@ ;; READ ONLY FUNCTIONS (define-read-only (is-executable) - (let - ( - (votingRecord (unwrap! (get-vote-totals) ERR_PANIC)) - (miaRecord (get mia votingRecord)) - (voteTotals (get totals votingRecord)) - ) - ;; check that there is at least one vote - (asserts! (or (> (get totalVotesYes voteTotals) u0) (> (get totalVotesNo voteTotals) u0)) ERR_VOTE_FAILED) - ;; check that the yes total is more than no total - (asserts! (> (get totalVotesYes voteTotals) (get totalVotesNo voteTotals)) ERR_VOTE_FAILED) - ;; check the "yes" votes are at least 25% of the total - (asserts! (>= (get totalAmountYes miaRecord) (/ (get totalAmountYes voteTotals) u4)) ERR_VOTE_FAILED) - ;; check that the voting period has ended - (asserts! (> block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) - ;; allow execution - (ok true) - ) + ;; no action to execute, this is a signal vote + (ok true) ) (define-read-only (is-vote-active) @@ -260,4 +238,4 @@ (define-private (scale-down (a uint)) (/ a VOTE_SCALE_FACTOR) -) \ No newline at end of file +) From 2b81601a96cd689d25c100e13ec4d0cf53806d17 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 2 Aug 2024 15:40:31 -0700 Subject: [PATCH 3/9] fix: adjust block height to be in future for tests All tests are expecting to use early stacking cycles in order to vote, but the contract sets the voting window too soon when it just goes off the block height it's deployed. For testing we extend the set blocks by u12600 which represents 6 * 2100 block cycle length. This should be removed for mainnet deployment and comments above include the correct code for deployment. --- contracts/proposals/ccip024-miamicoin-signal-vote.clar | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/proposals/ccip024-miamicoin-signal-vote.clar b/contracts/proposals/ccip024-miamicoin-signal-vote.clar index 889ed36..b933fbf 100644 --- a/contracts/proposals/ccip024-miamicoin-signal-vote.clar +++ b/contracts/proposals/ccip024-miamicoin-signal-vote.clar @@ -33,10 +33,12 @@ ;; vote block heights (define-data-var voteActive bool true) -;; start the vote when deployed -(define-data-var voteStart uint block-height) -;; end the vote after defined period -(define-data-var voteEnd uint (+ block-height VOTE_LENGTH)) +;; MAINNET: start the vote when deployed +;; (define-data-var voteStart uint block-height) +(define-data-var voteStart uint (+ block-height u12600)) +;; MAINNET: end the vote after defined period +;; (define-data-var voteEnd uint (+ block-height VOTE_LENGTH)) +(define-data-var voteEnd uint (+ block-height VOTE_LENGTH u12600)) ;; DATA MAPS From 11db5ef01018e2571dfb6baddcd857f6f58c7b97 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 2 Aug 2024 15:40:53 -0700 Subject: [PATCH 4/9] fix: add models and tests for ccip-024 --- Clarinet.toml | 5 + .../ccip024-miamicoin-signal-vote.model.ts | 78 ++++++++++++ ...est-ccip024-miamicoin-signal-vote-001.clar | 36 ++++++ .../ccip024-miamicoin-signal-vote.test.ts | 115 ++++++++++++++++++ utils/common.ts | 2 + 5 files changed, 236 insertions(+) create mode 100644 models/proposals/ccip024-miamicoin-signal-vote.model.ts create mode 100644 tests/contracts/proposals/test-ccip024-miamicoin-signal-vote-001.clar create mode 100644 tests/proposals/ccip024-miamicoin-signal-vote.test.ts diff --git a/Clarinet.toml b/Clarinet.toml index d1c404e..67ce4f0 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -577,6 +577,11 @@ path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar" clarity_version = 2 epoch = 2.4 +[contracts.test-ccip024-miamicoin-signal-vote-001] +path = "tests/contracts/proposals/test-ccip024-miamicoin-signal-vote-001.clar" +clarity_version = 2 +epoch = 2.4 + [repl] costs_version = 2 parser_version = 2 diff --git a/models/proposals/ccip024-miamicoin-signal-vote.model.ts b/models/proposals/ccip024-miamicoin-signal-vote.model.ts new file mode 100644 index 0000000..47c68f4 --- /dev/null +++ b/models/proposals/ccip024-miamicoin-signal-vote.model.ts @@ -0,0 +1,78 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 24000, + ERR_SAVING_VOTE, + ERR_VOTED_ALREADY, + ERR_NOTHING_STACKED, + ERR_USER_NOT_FOUND, + ERR_PROPOSAL_NOT_ACTIVE, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_VOTE_FAILED, +} + +export class CCIP024MiamiCoinSignalVote { + name = "ccip024-miamicoin-signal-vote"; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account) { + this.chain = chain; + this.deployer = deployer; + } + + // public functions + + execute(sender: Account) { + return Tx.contractCall(this.name, "execute", [types.principal(sender.address)], sender.address); + } + + voteOnProposal(sender: Account, vote: boolean) { + return Tx.contractCall(this.name, "vote-on-proposal", [types.bool(vote)], sender.address); + } + + // read-only functions + + isExecutable() { + return this.callReadOnlyFn("is-executable"); + } + + isVoteActive() { + return this.callReadOnlyFn("is-vote-active"); + } + + getProposalInfo() { + return this.callReadOnlyFn("get-proposal-info"); + } + + getVotePeriod() { + return this.callReadOnlyFn("get-vote-period"); + } + + getVoteTotalMia() { + return this.callReadOnlyFn("get-vote-total-mia"); + } + + getVoteTotalMiaOrDefault() { + return this.callReadOnlyFn("get-vote-total-mia-or-default"); + } + + getVoteTotals() { + return this.callReadOnlyFn("get-vote-totals"); + } + + getVoterInfo(userId: number) { + return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]); + } + + getMiaVote(userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-mia-vote", [types.uint(userId), types.bool(scaled)]); + } + + // read-only function helper + private callReadOnlyFn(method: string, args: Array = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/tests/contracts/proposals/test-ccip024-miamicoin-signal-vote-001.clar b/tests/contracts/proposals/test-ccip024-miamicoin-signal-vote-001.clar new file mode 100644 index 0000000..b0c9a66 --- /dev/null +++ b/tests/contracts/proposals/test-ccip024-miamicoin-signal-vote-001.clar @@ -0,0 +1,36 @@ +;; Title: Test Proposal for CCIP-024 +;; Version: 1.0.0 +;; Synopsis: Test proposal for CCIP-024 MiamiCoin Community Signal Vote +;; Description: +;; Sets up everything required for CCIP-024 + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; Set up MIA in city registry + (try! (contract-call? .ccd004-city-registry get-or-create-city-id "mia")) + + ;; Set activation details for MIA + (try! (contract-call? .ccd005-city-data set-activation-details u1 u1 u1 u5 u1)) + + ;; Set activation status for MIA + (try! (contract-call? .ccd005-city-data set-activation-status u1 true)) + + ;; Add MIA mining treasury + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining-v2 "mining")) + + ;; Add MIA stacking treasury + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-stacking "stacking")) + + ;; Mint MIA tokens to test users + (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) + (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + (try! (contract-call? .test-ccext-governance-token-mia mint u1000 'ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC)) + + ;; Add MIA token to stacking treasury allow list + (try! (contract-call? .ccd002-treasury-mia-stacking set-allowed .test-ccext-governance-token-mia true)) + + (ok true) + ) +) diff --git a/tests/proposals/ccip024-miamicoin-signal-vote.test.ts b/tests/proposals/ccip024-miamicoin-signal-vote.test.ts new file mode 100644 index 0000000..fc24578 --- /dev/null +++ b/tests/proposals/ccip024-miamicoin-signal-vote.test.ts @@ -0,0 +1,115 @@ +import { Account, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { constructAndPassProposal, PROPOSALS, mia } from "../../utils/common.ts"; +import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; +import { CCIP024MiamiCoinSignalVote } from "../../models/proposals/ccip024-miamicoin-signal-vote.model.ts"; + +Clarinet.test({ + name: "ccip-024: vote-on-proposal() succeeds for eligible voters", + fn(chain: Chain, accounts: Map) { + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + + // Initialize contracts and stack + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); + chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, 500, 10)]); + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + + // Act & Assert + const receipt = chain.mineBlock([ccip024.voteOnProposal(user1, true)]).receipts[0]; + receipt.result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccip-024: vote-on-proposal() fails for ineligible voters", + fn(chain: Chain, accounts: Map) { + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); + + const receipt = chain.mineBlock([ccip024.voteOnProposal(user1, true)]).receipts[0]; + receipt.result.expectErr().expectUint(CCIP024MiamiCoinSignalVote.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-024: vote-on-proposal() fails after voting period ends", + fn(chain: Chain, accounts: Map) { + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); + chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, 500, 10)]); + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 7 + 10); + + const receipt = chain.mineBlock([ccip024.voteOnProposal(user1, true)]).receipts[0]; + receipt.result.expectErr().expectUint(CCIP024MiamiCoinSignalVote.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-024: read-only functions return expected values", + fn(chain: Chain, accounts: Map) { + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + + // Setup + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); + chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, 500, 10), ccd007CityStacking.stack(user2, mia.cityName, 300, 10)]); + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + + // Vote + chain.mineBlock([ccip024.voteOnProposal(user1, true), ccip024.voteOnProposal(user2, false)]); + + // Assert + ccip024 + .getVoteTotalMia() + .result.expectSome() + .expectTuple({ + totalAmountYes: types.uint(500), + totalAmountNo: types.uint(300), + totalVotesYes: types.uint(1), + totalVotesNo: types.uint(1), + }); + + ccip024 + .getVoterInfo(1) + .result.expectSome() + .expectTuple({ + vote: types.bool(true), + mia: types.uint(500), + }); + + ccip024.isVoteActive().result.expectSome().expectBool(true); + + const voteInfo = ccip024.getVotePeriod().result.expectSome().expectTuple(); + voteInfo.length.expectUint(2016); + }, +}); + +Clarinet.test({ + name: "ccip-024: execute() and is-executable() always succeed", + fn(chain: Chain, accounts: Map) { + const sender = accounts.get("deployer")!; + const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); + + ccip024.isExecutable().result.expectOk().expectBool(true); + + const receipt = chain.mineBlock([ccip024.execute(sender)]).receipts[0]; + receipt.result.expectOk().expectBool(true); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index e302729..d9a2d88 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -44,6 +44,7 @@ export const PROPOSALS = { CCIP_020: ADDRESS.concat(".ccip020-graceful-protocol-shutdown"), CCIP_021: ADDRESS.concat(".ccip021-extend-sunset-period-2"), CCIP_022: ADDRESS.concat(".ccip022-treasury-redemption-nyc"), + CCIP_024: ADDRESS.concat(".ccip024-miamicoin-signal-vote"), TEST_CCD001_DIRECT_EXECUTE_001: ADDRESS.concat(".test-ccd001-direct-execute-001"), TEST_CCD001_DIRECT_EXECUTE_002: ADDRESS.concat(".test-ccd001-direct-execute-002"), TEST_CCD001_DIRECT_EXECUTE_003: ADDRESS.concat(".test-ccd001-direct-execute-003"), @@ -126,6 +127,7 @@ export const PROPOSALS = { TEST_CCIP022_TREASURY_REDEMPTION_NYC_003: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-003"), TEST_CCIP022_TREASURY_REDEMPTION_NYC_004: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-004"), TEST_CCIP022_TREASURY_REDEMPTION_NYC_005: ADDRESS.concat(".test-ccip022-treasury-redemption-nyc-005"), + TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001: ADDRESS.concat(".test-ccip024-miamicoin-signal-vote-001"), }; export const EXTERNAL = { From cada4aec1a51c355d98a6705d29c2b41ed721d9a Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 2 Aug 2024 16:19:09 -0700 Subject: [PATCH 5/9] fix: test uncovered paths --- .../ccip024-miamicoin-signal-vote.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/proposals/ccip024-miamicoin-signal-vote.test.ts b/tests/proposals/ccip024-miamicoin-signal-vote.test.ts index fc24578..4b570c4 100644 --- a/tests/proposals/ccip024-miamicoin-signal-vote.test.ts +++ b/tests/proposals/ccip024-miamicoin-signal-vote.test.ts @@ -23,6 +23,28 @@ Clarinet.test({ }, }); +Clarinet.test({ + name: "ccip-024: vote-on-proposal() succeeds if vote is changed", + fn(chain: Chain, accounts: Map) { + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + + // Initialize contracts and stack + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); + chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, 500, 10)]); + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + const receipt = chain.mineBlock([ccip024.voteOnProposal(user1, true)]).receipts[0]; + receipt.result.expectOk().expectBool(true); + + // Act & Assert + const receiptReverse = chain.mineBlock([ccip024.voteOnProposal(user1, false)]).receipts[0]; + receiptReverse.result.expectOk().expectBool(true); + }, +}); + Clarinet.test({ name: "ccip-024: vote-on-proposal() fails for ineligible voters", fn(chain: Chain, accounts: Map) { @@ -74,6 +96,14 @@ Clarinet.test({ chain.mineBlock([ccip024.voteOnProposal(user1, true), ccip024.voteOnProposal(user2, false)]); // Assert + ccip024 + .getVoteTotals() + .result.expectSome() + .expectTuple({ + totalVotesYes: types.uint(1), + totalVotesNo: types.uint(1), + }); + ccip024 .getVoteTotalMia() .result.expectSome() From c0302c61d71dc1430cdc9af1e0eaa010e13cea6d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 8 Aug 2024 16:35:31 -0700 Subject: [PATCH 6/9] fix: print voter info after successful tx --- contracts/proposals/ccip024-miamicoin-signal-vote.clar | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/proposals/ccip024-miamicoin-signal-vote.clar b/contracts/proposals/ccip024-miamicoin-signal-vote.clar index b933fbf..8234188 100644 --- a/contracts/proposals/ccip024-miamicoin-signal-vote.clar +++ b/contracts/proposals/ccip024-miamicoin-signal-vote.clar @@ -93,6 +93,11 @@ ) ;; update vote stats for MIA (update-city-votes MIA_ID miaVoteAmount vote true) + ;; print voter info + (print { + notification: "vote-on-ccip-024", + payload: (get-voter-info voterId) + }) (ok true) ) ;; if the voterRecord does not exist @@ -109,6 +114,11 @@ }) ERR_SAVING_VOTE) ;; update vote stats for MIA (update-city-votes MIA_ID miaVoteAmount vote false) + ;; print voter info + (print { + notification: "vote-on-ccip-024", + payload: (get-voter-info voterId) + }) (ok true) ) ) From 5587053d4124b16eb7f7e2ac1660aab5ca994f53 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 8 Aug 2024 19:18:56 -0700 Subject: [PATCH 7/9] fix: add hash from ccip-024 commit --- contracts/proposals/ccip024-miamicoin-signal-vote.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/proposals/ccip024-miamicoin-signal-vote.clar b/contracts/proposals/ccip024-miamicoin-signal-vote.clar index 8234188..bdd1fed 100644 --- a/contracts/proposals/ccip024-miamicoin-signal-vote.clar +++ b/contracts/proposals/ccip024-miamicoin-signal-vote.clar @@ -20,7 +20,7 @@ (define-constant CCIP_024 { name: "MiamiCoin Community Signal Vote", link: "https://github.com/citycoins/governance/blob/feat/add-ccip-024/ccips/ccip-024/ccip-024-miamicoin-community-signal-vote.md", - hash: "TBD", + hash: "2a32c503f434a73aa4d54555fc222b53755fa665", }) (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places From 14edf5d366b1d0a079133e965491b8e60089d3a5 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 9 Aug 2024 09:04:29 -0700 Subject: [PATCH 8/9] fix: make is-vote-active dependent on voteStart and voteEnd The logic can be simplified here since we have a set end block height for the vote, tests are updated as well. --- .../proposals/ccip024-miamicoin-signal-vote.clar | 11 +++++------ tests/proposals/ccip024-miamicoin-signal-vote.test.ts | 11 ++++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/contracts/proposals/ccip024-miamicoin-signal-vote.clar b/contracts/proposals/ccip024-miamicoin-signal-vote.clar index bdd1fed..617be47 100644 --- a/contracts/proposals/ccip024-miamicoin-signal-vote.clar +++ b/contracts/proposals/ccip024-miamicoin-signal-vote.clar @@ -32,7 +32,6 @@ ;; DATA VARS ;; vote block heights -(define-data-var voteActive bool true) ;; MAINNET: start the vote when deployed ;; (define-data-var voteStart uint block-height) (define-data-var voteStart uint (+ block-height u12600)) @@ -74,9 +73,7 @@ (voterRecord (map-get? UserVotes voterId)) ) ;; check if vote is active - (asserts! (var-get voteActive) ERR_PROPOSAL_NOT_ACTIVE) - ;; check if voting period has ended - (asserts! (<= block-height (var-get voteEnd)) ERR_PROPOSAL_NOT_ACTIVE) + (asserts! (is-vote-active) ERR_PROPOSAL_NOT_ACTIVE) ;; check if vote record exists for user (match voterRecord record ;; if the voterRecord exists @@ -133,8 +130,10 @@ ) (define-read-only (is-vote-active) - (some (var-get voteActive)) -) + (if (and (> block-height (var-get voteStart)) (<= block-height (var-get voteEnd))) + true + false +)) (define-read-only (get-proposal-info) (some CCIP_024) diff --git a/tests/proposals/ccip024-miamicoin-signal-vote.test.ts b/tests/proposals/ccip024-miamicoin-signal-vote.test.ts index 4b570c4..2ca2f87 100644 --- a/tests/proposals/ccip024-miamicoin-signal-vote.test.ts +++ b/tests/proposals/ccip024-miamicoin-signal-vote.test.ts @@ -85,15 +85,17 @@ Clarinet.test({ const user2 = accounts.get("wallet_2")!; const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); const ccip024 = new CCIP024MiamiCoinSignalVote(chain, sender); + const voteLength = 2016; // Setup chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP024_MIAMICOIN_SIGNAL_VOTE_001); chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, 500, 10), ccd007CityStacking.stack(user2, mia.cityName, 300, 10)]); + ccip024.isVoteActive().result.expectBool(false); chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); // Vote - chain.mineBlock([ccip024.voteOnProposal(user1, true), ccip024.voteOnProposal(user2, false)]); + const voteBlock = chain.mineBlock([ccip024.voteOnProposal(user1, true), ccip024.voteOnProposal(user2, false)]); // Assert ccip024 @@ -122,10 +124,13 @@ Clarinet.test({ mia: types.uint(500), }); - ccip024.isVoteActive().result.expectSome().expectBool(true); + ccip024.isVoteActive().result.expectBool(true); const voteInfo = ccip024.getVotePeriod().result.expectSome().expectTuple(); - voteInfo.length.expectUint(2016); + voteInfo.length.expectUint(voteLength); + + chain.mineEmptyBlockUntil(voteBlock.height + voteLength); + ccip024.isVoteActive().result.expectBool(false); }, }); From 6861ef267e2a8104e6655a27e48e8160a889acf0 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 9 Aug 2024 15:18:23 -0700 Subject: [PATCH 9/9] fix: add missing ccip024 contracts to legacy Clarinet.toml --- Clarinet-legacy.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Clarinet-legacy.toml b/Clarinet-legacy.toml index 7e95afb..fa7a4c1 100644 --- a/Clarinet-legacy.toml +++ b/Clarinet-legacy.toml @@ -179,6 +179,11 @@ path = "contracts/proposals/ccip022-treasury-redemption-nyc.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip024-miamicoin-signal-vote] +path = "contracts/proposals/ccip024-miamicoin-signal-vote.clar" +clarity_version = 2 +epoch = 2.4 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] @@ -555,6 +560,11 @@ path = "tests/contracts/proposals/test-ccip022-treasury-redemption-nyc-005.clar" clarity_version = 2 epoch = 2.4 +[contracts.test-ccip024-miamicoin-signal-vote-001] +path = "tests/contracts/proposals/test-ccip024-miamicoin-signal-vote-001.clar" +clarity_version = 2 +epoch = 2.4 + [repl] costs_version = 2 parser_version = 2