diff --git a/Clarinet.toml b/Clarinet.toml index e7b4b0f4..42fae9f4 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -92,6 +92,9 @@ path = "contracts/extensions/ccd002-treasury.clar" [contracts.ccd002-treasury-mia-mining-v2] path = "contracts/extensions/ccd002-treasury-v2.clar" +# [contracts.ccd002-treasury-mia-mining-v3] +# path = "contracts/extensions/ccd002-treasury-v3.clar" + [contracts.ccd002-treasury-mia-stacking] path = "contracts/extensions/ccd002-treasury.clar" @@ -101,6 +104,9 @@ path = "contracts/extensions/ccd002-treasury.clar" [contracts.ccd002-treasury-nyc-mining-v2] path = "contracts/extensions/ccd002-treasury-v2.clar" +# [contracts.ccd002-treasury-nyc-mining-v3] +# path = "contracts/extensions/ccd002-treasury-v3.clar" + [contracts.ccd002-treasury-nyc-stacking] path = "contracts/extensions/ccd002-treasury.clar" @@ -158,6 +164,11 @@ path = "contracts/proposals/ccip017-extend-sunset-period.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip021-extend-sunset-period-2] +path = "contracts/proposals/ccip021-extend-sunset-period-2.clar" +clarity_version = 2 +epoch = 2.4 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] diff --git a/contracts/extensions/ccd002-treasury-v3.clar b/contracts/extensions/ccd002-treasury-v3.clar new file mode 100644 index 00000000..0748342c --- /dev/null +++ b/contracts/extensions/ccd002-treasury-v3.clar @@ -0,0 +1,211 @@ +;; Title: CCD002 Treasury +;; Version: 3.0.0 +;; Summary: A treasury contract that can manage STX, SIP-009 NFTs, and SIP-010 FTs. +;; Description: An extension contract that holds assets on behalf of the DAO. +;; SIP-009 and SIP-010 assets must be allowed before they are supported. +;; Deposits can be made by anyone either by transferring to the contract or +;; using a deposit function below. +;; Withdrawals are restricted to the DAO through either extensions or proposals. +;; Stacking is enabled through PoX. + +;; TRAITS + +(impl-trait .extension-trait.extension-trait) +(impl-trait .stacking-trait.stacking-trait) +(impl-trait .ccd002-trait.ccd002-treasury-trait) +;; MAINNET: 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait +(use-trait ft-trait 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.sip-010-trait-ft-standard.sip-010-trait) +;; MAINNET: 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait +(use-trait nft-trait 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT.nft-trait.nft-trait) + +;; CONSTANTS + +(define-constant ERR_UNAUTHORIZED (err u2000)) +(define-constant ERR_UNKNOWN_ASSSET (err u2001)) +(define-constant TREASURY (as-contract tx-sender)) + +;; DATA MAPS + +(define-map AllowedAssets principal bool) + +;; PUBLIC FUNCTIONS + +(define-public (is-dao-or-extension) + (ok (asserts! (or (is-eq tx-sender .base-dao) + (contract-call? .base-dao is-extension contract-caller)) ERR_UNAUTHORIZED + )) +) + +(define-public (callback (sender principal) (memo (buff 34))) + (ok true) +) + +(define-public (set-allowed (token principal) (enabled bool)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "allow-asset", + enabled: enabled, + token: token + }) + (ok (map-set AllowedAssets token enabled)) + ) +) + +(define-public (set-allowed-list (allowList (list 100 {token: principal, enabled: bool}))) + (begin + (try! (is-dao-or-extension)) + (ok (map set-allowed-iter allowList)) + ) +) + +(define-public (deposit-stx (amount uint)) + (begin + (print { + event: "deposit-stx", + amount: amount, + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender + }) + (stx-transfer? amount tx-sender TREASURY) + ) +) + +(define-public (deposit-ft (ft ) (amount uint)) + (begin + (asserts! (is-allowed (contract-of ft)) ERR_UNKNOWN_ASSSET) + (print { + event: "deposit-ft", + amount: amount, + assetContract: (contract-of ft), + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender + }) + (contract-call? ft transfer amount tx-sender TREASURY none) + ) +) + +(define-public (deposit-nft (nft ) (id uint)) + (begin + (asserts! (is-allowed (contract-of nft)) ERR_UNKNOWN_ASSSET) + (print { + event: "deposit-nft", + assetContract: (contract-of nft), + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender, + tokenId: id, + }) + (contract-call? nft transfer id tx-sender TREASURY) + ) +) + +(define-public (withdraw-stx (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "withdraw-stx", + amount: amount, + caller: contract-caller, + recipient: recipient, + sender: tx-sender + }) + (as-contract (stx-transfer? amount TREASURY recipient)) + ) +) + +(define-public (withdraw-ft (ft ) (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (is-allowed (contract-of ft)) ERR_UNKNOWN_ASSSET) + (print { + event: "withdraw-ft", + assetContract: (contract-of ft), + caller: contract-caller, + recipient: recipient, + sender: tx-sender + }) + (as-contract (contract-call? ft transfer amount TREASURY recipient none)) + ) +) + +(define-public (withdraw-nft (nft ) (id uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (is-allowed (contract-of nft)) ERR_UNKNOWN_ASSSET) + (print { + event: "withdraw-nft", + assetContract: (contract-of nft), + caller: contract-caller, + recipient: recipient, + sender: tx-sender, + tokenId: id + }) + (as-contract (contract-call? nft transfer id TREASURY recipient)) + ) +) + +(define-public (delegate-stx (maxAmount uint) (to principal)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "delegate-stx", + amount: maxAmount, + caller: contract-caller, + delegate: to, + sender: tx-sender + }) + ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-4 + ;; TESTNET: 'ST000000000000000000002AMW42H.pox-4 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx maxAmount to none none)) + success (ok success) + err (err (to-uint err)) + ) + ) +) + +(define-public (revoke-delegate-stx) + (begin + (try! (is-dao-or-extension)) + (print { + event: "revoke-delegate-stx", + caller: contract-caller, + sender: tx-sender + }) + ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-4 + ;; TESTNET: 'ST000000000000000000002AMW42H.pox-4 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-4 revoke-delegate-stx)) + success (ok success) + err (err (to-uint err)) + ) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-allowed (assetContract principal)) + (default-to false (get-allowed-asset assetContract)) +) + +(define-read-only (get-allowed-asset (assetContract principal)) + (map-get? AllowedAssets assetContract) +) + +(define-read-only (get-balance-stx) + (stx-get-balance TREASURY) +) + +;; PRIVATE FUNCTIONS + +(define-private (set-allowed-iter (item {token: principal, enabled: bool})) + (begin + (print { + event: "allow-asset", + enabled: (get enabled item), + token: (get token item) + }) + (map-set AllowedAssets (get token item) (get enabled item)) + ) +) diff --git a/contracts/proposals/ccip021-extend-sunset-period-2.clar b/contracts/proposals/ccip021-extend-sunset-period-2.clar new file mode 100644 index 00000000..48e09498 --- /dev/null +++ b/contracts/proposals/ccip021-extend-sunset-period-2.clar @@ -0,0 +1,265 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip015-trait.ccip015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u2100)) +(define-constant ERR_VOTED_ALREADY (err u2101)) +(define-constant ERR_NOTHING_STACKED (err u2102)) +(define-constant ERR_USER_NOT_FOUND (err u2103)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u2104)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u2105)) +(define-constant ERR_NO_CITY_ID (err u2106)) +(define-constant ERR_VOTE_FAILED (err u2107)) + +;; CONSTANTS + +(define-constant SELF (as-contract tx-sender)) +(define-constant CCIP_021 { + name: "Extend Direct Execute Sunset Period 2", + link: "https://github.com/citycoins/governance/blob/feat/ccip-21/ccips/ccip-021/ccip-021-extend-direct-execute-sunset-period-2.md", + hash: "3af7199173df90463a0ba65541b53fa74e0914db", +}) +(define-constant SUNSET_BLOCK u173748) + +(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 u8916) ;; 0.8916 or 89.16% +;; MIA votes scaled to make 1 MIA = 1 NYC +;; full calculation available in CCIP-021 + +;; 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_021) +) + +(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 80 / first block BTC 834,050 STX 142,301 + ;; cycle 2 / u4500 used in tests + (cycle80Hash (unwrap! (get-block-hash u4500) none)) + (cycle80Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle80Amount (get stacked cycle80Data)) + ;; MAINNET: MIA cycle 81 / first block BTC 836,150 STX 143,989 + ;; cycle 3 / u6600 used in tests + (cycle81Hash (unwrap! (get-block-hash u6600) none)) + (cycle81Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle81Amount (get stacked cycle81Data)) + ;; MIA vote calculation + (avgStacked (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) + (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle80Amount u0) (> cycle81Amount 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 80 / first block BTC 834,050 STX 142,301 + ;; cycle 2 / u4500 used in tests + (cycle80Hash (unwrap! (get-block-hash u4500) none)) + (cycle80Data (at-block cycle80Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle80Amount (get stacked cycle80Data)) + ;; NYC cycle 81 / first block BTC 836,150 STX 143,989 + ;; cycle 3 / u6600 used in tests + (cycle81Hash (unwrap! (get-block-hash u6600) none)) + (cycle81Data (at-block cycle81Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle81Amount (get stacked cycle81Data)) + ;; NYC vote calculation + (scaledVote (/ (+ (scale-up cycle80Amount) (scale-up cycle81Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle80Amount u0) (> cycle81Amount 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/ccip021-extend-direct-execute-sunset-period-2.model.ts b/models/proposals/ccip021-extend-direct-execute-sunset-period-2.model.ts new file mode 100644 index 00000000..e83fb669 --- /dev/null +++ b/models/proposals/ccip021-extend-direct-execute-sunset-period-2.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 = 2100, + 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 CCIP021ExtendDirectExecuteSunsetPeriod { + name = PROPOSALS.CCIP_021; + 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/ccip021-extend-sunset-period-2.test.ts b/tests/proposals/ccip021-extend-sunset-period-2.test.ts new file mode 100644 index 00000000..712dc134 --- /dev/null +++ b/tests/proposals/ccip021-extend-sunset-period-2.test.ts @@ -0,0 +1,590 @@ +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 { CCIP021ExtendDirectExecuteSunsetPeriod } from "../../models/proposals/ccip021-extend-direct-execute-sunset-period-2.model.ts"; + +const TARGET_SUNSET_BLOCK = 173748; + +Clarinet.test({ + name: "ccip-021: 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-021 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP021ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, false), ccip021ExtendDirectExecuteSunsetPeriod.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(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(1)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 1, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 1, true)); + console.log("user 2:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 2)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(2)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 2, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, 2, true)); + */ + + // execute ccip-021 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP021ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.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(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(userId)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, userId, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, userId, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, userId)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(userId)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, userId, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, userId, true)); + */ + + // check vote is active + ccip021ExtendDirectExecuteSunsetPeriod.isVoteActive().result.expectSome().expectBool(true); + // check proposal info + const proposalInfo = { + hash: types.ascii("3af7199173df90463a0ba65541b53fa74e0914db"), + link: types.ascii("https://github.com/citycoins/governance/blob/feat/ccip-21/ccips/ccip-021/ccip-021-extend-direct-execute-sunset-period-2.md"), + name: types.ascii("Extend Direct Execute Sunset Period 2"), + }; + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getProposalInfo().result.expectSome().expectTuple(), proposalInfo); + // check vote period is not set (end unknown) + ccip021ExtendDirectExecuteSunsetPeriod.getVotePeriod().result.expectNone(); + + // execute ccip-021 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // 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(ccip021ExtendDirectExecuteSunsetPeriod.getVotePeriod().result.expectSome().expectTuple(), votingPeriod); + // check vote is no longer active + ccip021ExtendDirectExecuteSunsetPeriod.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); + // check for print event in execute block + const ccip021PrintEvent = `{event: ${types.ascii("set-sunset-block")}, height: ${types.uint(TARGET_SUNSET_BLOCK)}}`; + assertEquals(block.receipts[2].events[1].contract_event.value, ccip021PrintEvent); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip021ExtendDirectExecuteSunsetPeriod.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(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user1Id, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user1Id, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user1Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user1Id, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user1Id, true)); + console.log("\nuser 2 mia:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user2Id, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getMiaVote(mia.cityId, user2Id, true)); + console.log("\nuser 2 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user2Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user2Id, false)); + console.log(ccip021ExtendDirectExecuteSunsetPeriod.getNycVote(nyc.cityId, user2Id, true)); + */ + + // execute ccip-021 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, false), ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, true)]); + + // switch yes and no vote + const votingBlockReverse = chain.mineBlock([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip021ExtendDirectExecuteSunsetPeriod.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-021 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user3, true)]); + + // assert + //console.log(`votingBlock: ${JSON.stringify(votingBlock, null, 2)}`); + votingBlock.receipts[0].result.expectErr().expectUint(CCIP021ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + // execute ccip-021 + passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // act + const votingBlock2 = chain.mineBlock([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true)]); + + // assert + votingBlock2.receipts[0].result.expectErr().expectUint(CCIP021ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + // act + const votingBlock2 = chain.mineBlock([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true)]); + + // assert + votingBlock2.receipts[0].result.expectErr().expectUint(CCIP021ExtendDirectExecuteSunsetPeriod.ErrCode.ERR_VOTED_ALREADY); + }, +}); + +Clarinet.test({ + name: "ccip-021: 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 ccip021ExtendDirectExecuteSunsetPeriod = new CCIP021ExtendDirectExecuteSunsetPeriod(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-021 + 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([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, false), ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, true)]); + + // assert + + // overall totals + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getVoteTotals().result.expectSome().expectTuple(), { + noTotal: types.uint(945), + noVotes: types.uint(1), + yesTotal: types.uint(472), + yesVotes: types.uint(1), + }); + + // user 1 + + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user1Id).result.expectTuple(), { + claimable: types.uint(0), + stacked: types.uint(500), + }); + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id).result.expectSome().expectTuple(), { + mia: types.uint(445), + nyc: types.uint(500), + total: types.uint(945), + vote: types.bool(false), + }); + + // user 2 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id).result.expectTuple(), { + claimable: types.uint(0), + stacked: types.uint(250), + }); + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id).result.expectSome().expectTuple(), { + mia: types.uint(222), + nyc: types.uint(250), + total: types.uint(472), + vote: types.bool(true), + }); + + // act + + // switch yes and no vote + const votingBlockReverse = chain.mineBlock([ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user1, true), ccip021ExtendDirectExecuteSunsetPeriod.voteOnProposal(user2, false)]); + + // assert + + // overall totals + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getVoteTotals().result.expectSome().expectTuple(), { + noTotal: types.uint(472), + noVotes: types.uint(1), + yesTotal: types.uint(945), + yesVotes: types.uint(1), + }); + // user 1 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user1Id).result.expectTuple(), { + claimable: types.uint(0), + stacked: types.uint(500), + }); + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user1Id).result.expectSome().expectTuple(), { + mia: types.uint(445), + nyc: types.uint(500), + total: types.uint(945), + vote: types.bool(true), + }); + // user 2 + assertEquals(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id).result.expectTuple(), { + claimable: types.uint(0), + stacked: types.uint(250), + }); + assertEquals(ccip021ExtendDirectExecuteSunsetPeriod.getVoterInfo(user2Id).result.expectSome().expectTuple(), { + mia: types.uint(222), + nyc: types.uint(250), + total: types.uint(472), + vote: types.bool(false), + }); + + // execute ccip-021 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_021); + + // assert + block.receipts[2].result.expectOk().expectUint(3); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index 098bbbee..4df0b80c 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -40,6 +40,7 @@ export const PROPOSALS = { CCIP_014: ADDRESS.concat(".ccip014-pox-3"), CCIP_014_V2: ADDRESS.concat(".ccip014-pox-3-v2"), CCIP_017: ADDRESS.concat(".ccip017-extend-sunset-period"), + CCIP_021: ADDRESS.concat(".ccip021-extend-sunset-period-2"), 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"),