From 7dad7d6f30acac9a6891f3f297734dbdad7b0613 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Fri, 1 Sep 2023 14:42:29 -0700 Subject: [PATCH] fix: add ccip017 contract, model, and test --- Clarinet.toml | 5 + ...7-extend-direct-execute-sunset-period.clar | 265 +++++++++ ...tend-direct-execute-sunset-period.model.ts | 73 +++ ...xtend-direct-execute-sunset-period.test.ts | 542 ++++++++++++++++++ utils/common.ts | 1 + 5 files changed, 886 insertions(+) create mode 100644 contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar create mode 100644 models/proposals/ccip017-extend-direct-execute-sunset-period.model.ts create mode 100644 tests/proposals/ccip017-extend-direct-execute-sunset-period.test.ts diff --git a/Clarinet.toml b/Clarinet.toml index 9d1ea6ee..8fd0eb48 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -153,6 +153,11 @@ path = "contracts/proposals/ccip014-pox-3-v2.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip017] +path = "contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar" +clarity_version = 2 +epoch = 2.4 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] diff --git a/contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar b/contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar new file mode 100644 index 00000000..b229c64f --- /dev/null +++ b/contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar @@ -0,0 +1,265 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip-015-trait.ccip-015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u1700)) +(define-constant ERR_VOTED_ALREADY (err u1701)) +(define-constant ERR_NOTHING_STACKED (err u1702)) +(define-constant ERR_USER_NOT_FOUND (err u1703)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u1704)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u1705)) +(define-constant ERR_NO_CITY_ID (err u1706)) +(define-constant ERR_VOTE_FAILED (err u1707)) + +;; CONSTANTS + +(define-constant SELF (as-contract tx-sender)) +(define-constant CCIP_017 { + name: "Extend Direct Execute Sunset Period", + link: "https://github.com/citycoins/governance/blob/feat/add-ccip-017/ccips/ccip-017/ccip-017-extend-direct-execute-sunset-period.md", + hash: "7ddbf6152790a730faa059b564a8524abc3c70d3", +}) +(define-constant SUNSET_BLOCK u147828) + +(define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places +(define-constant MIA_SCALE_BASE (pow u10 u4)) ;; 4 decimal places +(define-constant MIA_SCALE_FACTOR u8823) ;; 0.8823 or 88.23% +;; MIA votes scaled to make 1 MIA = 1 NYC +;; full calculation available in CCIP-017 + +;; DATA VARS + +;; vote block heights +(define-data-var voteActive bool true) +(define-data-var voteStart uint u0) +(define-data-var voteEnd uint u0) + +(var-set voteStart block-height) + +;; vote tracking +(define-data-var yesVotes uint u0) +(define-data-var yesTotal uint u0) +(define-data-var noVotes uint u0) +(define-data-var noTotal uint u0) + +;; DATA MAPS + +(define-map UserVotes + uint ;; user ID + { ;; vote + vote: bool, + mia: uint, + nyc: uint, + total: uint, + } +) + +;; PUBLIC FUNCTIONS + +(define-public (execute (sender principal)) + (begin + ;; check vote complete/passed + (try! (is-executable)) + ;; update vote variables + (var-set voteEnd block-height) + (var-set voteActive false) + ;; extend sunset height in ccd001-direct-execute + (try! (contract-call? .ccd001-direct-execute set-sunset-block SUNSET_BLOCK)) + (ok true) + ) +) + +(define-public (vote-on-proposal (vote bool)) + (let + ( + (miaId (unwrap! (contract-call? .ccd004-city-registry get-city-id "mia") ERR_NO_CITY_ID)) + (nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_NO_CITY_ID)) + (voterId (unwrap! (contract-call? .ccd003-user-registry get-user-id contract-caller) ERR_USER_NOT_FOUND)) + (voterRecord (map-get? UserVotes voterId)) + ) + ;; check that proposal is active + (asserts! (var-get voteActive) ERR_PROPOSAL_NOT_ACTIVE) + ;; check if vote record exists + (match voterRecord record + ;; if the voterRecord exists + (begin + ;; check vote is not the same as before + (asserts! (not (is-eq (get vote record) vote)) ERR_VOTED_ALREADY) + ;; record the new vote for the user + (map-set UserVotes voterId + (merge record { vote: vote }) + ) + ;; update the overall vote totals + (if vote + (begin + (var-set yesVotes (+ (var-get yesVotes) u1)) + (var-set yesTotal (+ (var-get yesTotal) (get total record))) + (var-set noVotes (- (var-get noVotes) u1)) + (var-set noTotal (- (var-get noTotal) (get total record))) + ) + (begin + (var-set yesVotes (- (var-get yesVotes) u1)) + (var-set yesTotal (- (var-get yesTotal) (get total record))) + (var-set noVotes (+ (var-get noVotes) u1)) + (var-set noTotal (+ (var-get noTotal) (get total record))) + ) + ) + ) + ;; if the voterRecord does not exist + (let + ( + (scaledVoteMia (default-to u0 (get-mia-vote miaId voterId true))) + (scaledVoteNyc (default-to u0 (get-nyc-vote nycId voterId true))) + (voteMia (scale-down scaledVoteMia)) + (voteNyc (scale-down scaledVoteNyc)) + (voteTotal (+ voteMia voteNyc)) + ) + ;; record the vote for the user + (map-insert UserVotes voterId { + vote: vote, + mia: voteMia, + nyc: voteNyc, + total: voteTotal, + }) + ;; update the overall vote totals + (if vote + (begin + (var-set yesVotes (+ (var-get yesVotes) u1)) + (var-set yesTotal (+ (var-get yesTotal) voteTotal)) + ) + (begin + (var-set noVotes (+ (var-get noVotes) u1)) + (var-set noTotal (+ (var-get noTotal) voteTotal)) + ) + ) + ) + ) + ;; print voter information + (print (map-get? UserVotes voterId)) + ;; print vote totals + (print (get-vote-totals)) + (ok true) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-executable) + (begin + ;; check that there is at least one vote + (asserts! (or (> (var-get yesVotes) u0) (> (var-get noVotes) u0)) ERR_VOTE_FAILED) + ;; check that yes total is more than no total + (asserts! (> (var-get yesTotal) (var-get noTotal)) ERR_VOTE_FAILED) + (ok true) + ) +) + +(define-read-only (is-vote-active) + (some (var-get voteActive)) +) + +(define-read-only (get-proposal-info) + (some CCIP_017) +) + +(define-read-only (get-vote-period) + (if (and + (> (var-get voteStart) u0) + (> (var-get voteEnd) u0)) + ;; if both are set, return values + (some { + startBlock: (var-get voteStart), + endBlock: (var-get voteEnd), + length: (- (var-get voteEnd) (var-get voteStart)) + }) + ;; else return none + none + ) +) + +(define-read-only (get-vote-totals) + (some { + yesVotes: (var-get yesVotes), + yesTotal: (var-get yesTotal), + noVotes: (var-get noVotes), + noTotal: (var-get noTotal) + }) +) + +(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 (cityId uint) (userId uint) (scaled bool)) + (let + ( + ;; MAINNET: MIA cycle 64 / first block BTC 800,450 STX 114,689 + ;; cycle 2 / u4500 used in tests + (cycle64Hash (unwrap! (get-block-hash u4500) none)) + (cycle64Data (at-block cycle64Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle64Amount (get stacked cycle64Data)) + ;; MAINNET: MIA cycle 65 / first block BTC 804,649 STX 118,282 + ;; cycle 3 / u6600 used in tests + (cycle65Hash (unwrap! (get-block-hash u6600) none)) + (cycle65Data (at-block cycle65Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle65Amount (get stacked cycle65Data)) + ;; MIA vote calculation + (avgStacked (/ (+ (scale-up cycle64Amount) (scale-up cycle65Amount)) u2)) + (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle64Amount u0) (> cycle65Amount u0)) none) + ;; return scaled or unscaled value + (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) + ) +) + +;; NYC vote calculation +;; returns (some uint) or (none) +;; optionally scaled by VOTE_SCALE_FACTOR (10^6) +(define-read-only (get-nyc-vote (cityId uint) (userId uint) (scaled bool)) + (let + ( + ;; NYC cycle 64 / first block BTC 800,450 STX 114,689 + ;; cycle 2 / u4500 used in tests + (cycle64Hash (unwrap! (get-block-hash u4500) none)) + (cycle64Data (at-block cycle64Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle64Amount (get stacked cycle64Data)) + ;; NYC cycle 65 / first block BTC 804,649 STX 118,282 + ;; cycle 3 / u6600 used in tests + (cycle65Hash (unwrap! (get-block-hash u6600) none)) + (cycle65Data (at-block cycle65Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle65Amount (get stacked cycle65Data)) + ;; NYC vote calculation + (scaledVote (/ (+ (scale-up cycle64Amount) (scale-up cycle65Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle64Amount u0) (> cycle65Amount u0)) none) + ;; return scaled or unscaled value + (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) + ) +) + +;; PRIVATE FUNCTIONS + +;; 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) +) + diff --git a/models/proposals/ccip017-extend-direct-execute-sunset-period.model.ts b/models/proposals/ccip017-extend-direct-execute-sunset-period.model.ts new file mode 100644 index 00000000..84142014 --- /dev/null +++ b/models/proposals/ccip017-extend-direct-execute-sunset-period.model.ts @@ -0,0 +1,73 @@ +import { PROPOSALS } from "../../utils/common.ts"; +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 1700, + ERR_VOTED_ALREADY, + ERR_NOTHING_STACKED, + ERR_USER_NOT_FOUND, + ERR_PROPOSAL_NOT_ACTIVE, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_NO_CITY_ID, + ERR_VOTE_FAILED, +} + +export class CCIP017ExtendDirectExecuteSunsetPeriod { + name = PROPOSALS.CCIP_017; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account) { + this.chain = chain; + this.deployer = deployer; + } + + // public functions + + // execute() excluded since called by passProposal and CCD001 + + 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"); + } + + getVoteTotals() { + return this.callReadOnlyFn("get-vote-totals"); + } + + getVoterInfo(userId: number) { + return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]); + } + + getMiaVote(cityId: number, userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-mia-vote", [types.uint(cityId), types.uint(userId), types.bool(scaled)]); + } + + getNycVote(cityId: number, userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-nyc-vote", [types.uint(cityId), 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/proposals/ccip017-extend-direct-execute-sunset-period.test.ts b/tests/proposals/ccip017-extend-direct-execute-sunset-period.test.ts new file mode 100644 index 00000000..83424bb0 --- /dev/null +++ b/tests/proposals/ccip017-extend-direct-execute-sunset-period.test.ts @@ -0,0 +1,542 @@ +import { Account, Clarinet, Chain, types, assertEquals } from "../../utils/deps.ts"; +import { constructAndPassProposal, mia, nyc, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; +import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; +import { CCIP017ExtendDirectExecuteSunsetPeriod } from "../../models/proposals/ccip017-extend-direct-execute-sunset-period.model.ts"; + +Clarinet.test({ + name: "ccip-017: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map) { + // arrange + + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + + // act + + // execute ccip-017 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP017ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-017: execute() fails with ERR_VOTE_FAILED if there are more no than yes votes", + fn(chain: Chain, accounts: Map) { + // arrange + 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 ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // add stacking treasury in city data + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_007); + // mints mia to user1 and user2 + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_009); + // adds the token contract to the treasury allow list + passProposal(chain, accounts, PROPOSALS.TEST_CCD007_CITY_STACKING_010); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + stackingBlock.receipts[1].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute two no votes + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, false), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + /* double check voting data + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + console.log("user 1:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 1)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(1)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 1, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 1, true)); + console.log("user 2:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 2)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(2)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 2, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 2, true)); + */ + + // execute ccip-017 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP017ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-017: execute() succeeds if there is a single yes vote", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute single yes vote + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true)]); + + /* double check voting data + const cycleId = 2; + const userId = 2; + console.log(`\nconstruct block:\n${JSON.stringify(constructBlock, null, 2)}`); + console.log(`\nmining block:\n${JSON.stringify(miningBlock, null, 2)}`); + console.log(`\nstacking block:\n${JSON.stringify(stackingBlock, null, 2)}`); + console.log(`\nvoting block:\n${JSON.stringify(votingBlock, null, 2)}`); + console.log("\nuser 1 mia:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, cycleId, userId)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(userId)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, userId, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, userId, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, userId)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(userId)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, userId, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, userId, true)); + */ + + // check vote is active + ccip017ExtendDirectExecuteSunsetPeriod.isVoteActive().result.expectSome().expectBool(true); + // check proposal info + const proposalInfo = { + hash: types.ascii("7ddbf6152790a730faa059b564a8524abc3c70d3"), + link: types.ascii("https://github.com/citycoins/governance/blob/feat/add-ccip-017/ccips/ccip-017/ccip-017-extend-direct-execute-sunset-period.md"), + name: types.ascii("Extend Direct Execute Sunset Period"), + }; + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getProposalInfo().result.expectSome().expectTuple(), proposalInfo); + // check vote period is not set (end unknown) + ccip017ExtendDirectExecuteSunsetPeriod.getVotePeriod().result.expectNone(); + + // execute ccip-017 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // assert + // check vote period is set and returns + const start = constructBlock.height - CCD007CityStacking.FIRST_STACKING_BLOCK - 1; + const end = votingBlock.height; + const votingPeriod = { + startBlock: types.uint(start), + endBlock: types.uint(end), + length: types.uint(end - start), + }; + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVotePeriod().result.expectSome().expectTuple(), votingPeriod); + // check vote is no longer active + ccip017ExtendDirectExecuteSunsetPeriod.isVoteActive().result.expectSome().expectBool(false); + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + // check that proposal executed + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-017: execute() succeeds if there are more yes than no votes", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + /* double check voting data + const cycleId = 2; + const user1Id = 2; + const user2Id = 3; + console.log(`\nconstruct block:\n${JSON.stringify(constructBlock, null, 2)}`); + console.log(`\nmining block:\n${JSON.stringify(miningBlock, null, 2)}`); + console.log(`\nstacking block:\n${JSON.stringify(stackingBlock, null, 2)}`); + console.log(`\nvoting block:\n${JSON.stringify(votingBlock, null, 2)}`); + console.log("\nuser 1 mia:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, cycleId, user1Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user1Id, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user1Id, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user1Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user1Id, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user1Id, true)); + console.log("\nuser 2 mia:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user2Id, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user2Id, true)); + console.log("\nuser 2 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user2Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user2Id, false)); + console.log(ccip017ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user2Id, true)); + */ + + // execute ccip-017 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-017: execute() succeeds if there are more yes than no votes after a reversal", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, false), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, true)]); + + // switch yes and no vote + const votingBlockReverse = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + /* double check voting data + console.log(`\nvoting block:\n${JSON.stringify(votingBlock, null, 2)}`); + console.log(`\nvoting block reverse:\n${JSON.stringify(votingBlockReverse, null, 2)}`); + */ + + // execute ccip-017 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-017: vote-on-proposal() fails with ERR_USER_NOT_FOUND if user is not registered in ccd003-user-registry", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const user3 = accounts.get("wallet_3")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute yes and no vote + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user3, true)]); + + // assert + //console.log(`votingBlock: ${JSON.stringify(votingBlock, null, 2)}`); + votingBlock.receipts[0].result.expectErr().expectUint(CCIP017ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-017: vote-on-proposal() fails with ERR_PROPOSAL_NOT_ACTIVE if called after the vote ends", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + // execute ccip-017 + passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // act + const votingBlock2 = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true)]); + + // assert + votingBlock2.receipts[0].result.expectErr().expectUint(CCIP017ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-017: vote-on-proposal() fails with ERR_VOTED_ALREADY if user already voted with the same value", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + // act + const votingBlock2 = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true)]); + + // assert + votingBlock2.receipts[0].result.expectErr().expectUint(CCIP017ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_VOTED_ALREADY); + }, +}); + +Clarinet.test({ + name: "ccip-017: read-only functions return expected values before/after reversal", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip017ExtendDirectExecuteSunsetPeriod = new CCIP017ExtendDirectExecuteSunsetPeriod(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + const cycleId = 2; + const user1Id = 2; + const user2Id = 3; + + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for ccip-017 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlock = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + + // stack first cycle u1, last cycle u10 + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked / 2, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked / 2, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + + // progress the chain to cycle 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, false), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, true)]); + + // assert + + // overall totals + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVoteTotals().result.expectSome().expectTuple(), { noTotal: types.uint(938), noVotes: types.uint(1), yesTotal: types.uint(469), yesVotes: types.uint(1) }); + // user 1 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user1Id).result.expectTuple(), { claimable: types.uint(0), stacked: types.uint(500) }); + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id).result.expectSome().expectTuple(), { mia: types.uint(438), nyc: types.uint(500), total: types.uint(938), vote: types.bool(false) }); + // user 2 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id).result.expectTuple(), { claimable: types.uint(0), stacked: types.uint(250) }); + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id).result.expectSome().expectTuple(), { mia: types.uint(219), nyc: types.uint(250), total: types.uint(469), vote: types.bool(true) }); + + // act + + // switch yes and no vote + const votingBlockReverse = chain.mineBlock([ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip017ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + // assert + + // overall totals + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVoteTotals().result.expectSome().expectTuple(), { noTotal: types.uint(469), noVotes: types.uint(1), yesTotal: types.uint(938), yesVotes: types.uint(1) }); + // user 1 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user1Id).result.expectTuple(), { claimable: types.uint(0), stacked: types.uint(500) }); + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id).result.expectSome().expectTuple(), { mia: types.uint(438), nyc: types.uint(500), total: types.uint(938), vote: types.bool(true) }); + // user 2 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id).result.expectTuple(), { claimable: types.uint(0), stacked: types.uint(250) }); + assertEquals(ccip017ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id).result.expectSome().expectTuple(), { mia: types.uint(219), nyc: types.uint(250), total: types.uint(469), vote: types.bool(false) }); + + // execute ccip-017 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_017); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index cf496c12..60b1869d 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -39,6 +39,7 @@ export const PROPOSALS = { CCIP_013: ADDRESS.concat(".ccip013-migration"), CCIP_014: ADDRESS.concat(".ccip014-pox-3"), CCIP_014_V2: ADDRESS.concat(".ccip014-pox-3-v2"), + CCIP_017: ADDRESS.concat(".ccip017"), 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"),