From a053d6f621efdbdcd866903ecd06ca26d2c7fd8f Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 29 Mar 2023 17:08:31 -0700 Subject: [PATCH 01/47] fix: add template for new contracts based on originals Also adds a proposal for ccip-014 which would be the proposal that activates stacking through pox-2. --- Clarinet.toml | 9 + contracts/extensions/ccd002-treasury-v2.clar | 204 ++++++++++++ .../extensions/ccd006-citycoin-mining-v2.clar | 294 ++++++++++++++++++ contracts/proposals/ccip014-pox-2.clar | 5 + 4 files changed, 512 insertions(+) create mode 100644 contracts/extensions/ccd002-treasury-v2.clar create mode 100644 contracts/extensions/ccd006-citycoin-mining-v2.clar create mode 100644 contracts/proposals/ccip014-pox-2.clar diff --git a/Clarinet.toml b/Clarinet.toml index e579de0b..782f8ffb 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -89,12 +89,18 @@ path = "contracts/extensions/ccd001-direct-execute.clar" [contracts.ccd002-treasury-mia-mining] path = "contracts/extensions/ccd002-treasury.clar" +[contracts.ccd002-treasury-mia-mining-v2] +path = "contracts/extensions/ccd002-treasury-v2.clar" + [contracts.ccd002-treasury-mia-stacking] path = "contracts/extensions/ccd002-treasury.clar" [contracts.ccd002-treasury-nyc-mining] path = "contracts/extensions/ccd002-treasury.clar" +[contracts.ccd002-treasury-nyc-mining-v2] +path = "contracts/extensions/ccd002-treasury-v2.clar" + [contracts.ccd002-treasury-nyc-stacking] path = "contracts/extensions/ccd002-treasury.clar" @@ -134,6 +140,9 @@ path = "contracts/proposals/ccip013-migration.clar" [contracts.ccip013-activation] path = "contracts/proposals/ccip013-activation.clar" +[contracts.ccip014-pox-2] +path = "contracts/proposals/ccip014-pox-2.clar" + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] diff --git a/contracts/extensions/ccd002-treasury-v2.clar b/contracts/extensions/ccd002-treasury-v2.clar new file mode 100644 index 00000000..528c6ff7 --- /dev/null +++ b/contracts/extensions/ccd002-treasury-v2.clar @@ -0,0 +1,204 @@ +;; Title: CCD002 Treasury +;; Version: 2.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 <ft-trait>) (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 <nft-trait>) (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 <ft-trait>) (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 <nft-trait>) (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 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox 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 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox 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/extensions/ccd006-citycoin-mining-v2.clar b/contracts/extensions/ccd006-citycoin-mining-v2.clar new file mode 100644 index 00000000..458146e9 --- /dev/null +++ b/contracts/extensions/ccd006-citycoin-mining-v2.clar @@ -0,0 +1,294 @@ +;; Title: CCD006 CityCoin Mining +;; Version: 2.0.0 +;; Summary: A central city mining contract for the CityCoins protocol. +;; Description: An extension that provides a mining interface per city, in which each mining participant spends STX per block for a weighted chance to mint new CityCoins per the issuance schedule. + +;; TRAITS + +(impl-trait .extension-trait.extension-trait) +(impl-trait .ccd006-trait.ccd006-citycoin-mining-trait) + +;; CONSTANTS + +(define-constant ERR_UNAUTHORIZED (err u6000)) +(define-constant ERR_INVALID_CITY (err u6001)) +(define-constant ERR_NO_ACTIVATION_DETAILS (err u6002)) +(define-constant ERR_INACTIVE_CITY (err u6003)) +(define-constant ERR_INVALID_USER (err u6004)) +(define-constant ERR_INVALID_TREASURY (err u6005)) +(define-constant ERR_INVALID_DELAY (err u6006)) +(define-constant ERR_INVALID_COMMITS (err u6007)) +(define-constant ERR_NOT_ENOUGH_FUNDS (err u6008)) +(define-constant ERR_ALREADY_MINED (err u6009)) +(define-constant ERR_REWARD_IMMATURE (err u6010)) +(define-constant ERR_NO_VRF_SEED (err u6011)) +(define-constant ERR_DID_NOT_MINE (err u6012)) +(define-constant ERR_NO_MINER_DATA (err u6013)) +(define-constant ERR_ALREADY_CLAIMED (err u6014)) +(define-constant ERR_MINER_NOT_WINNER (err u6015)) +(define-constant ERR_MINING_DISABLED (err u6016)) + +;; DATA VARS + +(define-data-var miningEnabled bool true) +(define-data-var rewardDelay uint u100) + +;; DATA MAPS + +(define-map MiningStats + { cityId: uint, height: uint } + { miners: uint, amount: uint, claimed: bool } +) + +(define-map Miners + { cityId: uint, height: uint, userId: uint } + { commit: uint, low: uint, high: uint, winner: bool } +) + +(define-map HighValues + { cityId: uint, height: uint } + uint +) + +(define-map Winners + { cityId: uint, height: uint } + uint +) + +;; 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-reward-delay (delay uint)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "set-reward-delay", + rewardDelay: delay + }) + (asserts! (> delay u0) ERR_INVALID_DELAY) + (ok (var-set rewardDelay delay)) + ) +) + +(define-public (set-mining-enabled (status bool)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "set-mining-enabled", + miningEnabled: status + }) + (ok (var-set miningEnabled status)) + ) +) + +(define-public (mine (cityName (string-ascii 10)) (amounts (list 200 uint))) + (let + ( + (cityId (unwrap! (contract-call? .ccd004-city-registry get-city-id cityName) ERR_INVALID_CITY)) + (cityInfo (contract-call? .ccd005-city-data get-city-info cityId "mining")) + (cityDetails (unwrap! (get details cityInfo) ERR_NO_ACTIVATION_DETAILS)) + (cityTreasury (unwrap! (get treasury cityInfo) ERR_INVALID_TREASURY)) + (user tx-sender) + (userId (try! (as-contract (contract-call? .ccd003-user-registry get-or-create-user-id user)))) + (totalAmount (fold + amounts u0)) + ) + (asserts! (var-get miningEnabled) ERR_MINING_DISABLED) + (asserts! (get activatedAt cityInfo) ERR_INACTIVE_CITY) + (asserts! (>= (stx-get-balance tx-sender) totalAmount) ERR_NOT_ENOUGH_FUNDS) + (asserts! (> (len amounts) u0) ERR_INVALID_COMMITS) + (try! (fold mine-block amounts (ok { + cityId: cityId, + userId: userId, + height: block-height, + totalAmount: u0, + }))) + (print { + event: "mining", + cityId: cityId, + cityName: cityName, + cityTreasury: cityTreasury, + firstBlock: block-height, + lastBlock: (- (+ block-height (len amounts)) u1), + totalAmount: totalAmount, + totalBlocks: (len amounts), + userId: userId + }) + (stx-transfer? totalAmount tx-sender cityTreasury) + ) +) + +(define-public (claim-mining-reward (cityName (string-ascii 10)) (claimHeight uint)) + (let + ( + (cityId (unwrap! (contract-call? .ccd004-city-registry get-city-id cityName) ERR_INVALID_CITY)) + (maturityHeight (+ (get-reward-delay) claimHeight)) + (isMature (asserts! (> block-height maturityHeight) ERR_REWARD_IMMATURE)) + (userId (unwrap! (contract-call? .ccd003-user-registry get-user-id tx-sender) ERR_INVALID_USER)) + (blockStats (get-mining-stats cityId claimHeight)) + (minerStats (get-miner cityId claimHeight userId)) + ;; MAINNET: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 + ;; TESTNET: 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 + (vrfSample (unwrap! (contract-call? .citycoin-vrf-v2 get-save-rnd maturityHeight) ERR_NO_VRF_SEED)) + (commitTotal (get-high-value cityId claimHeight)) + (commitValid (asserts! (> commitTotal u0) ERR_NO_MINER_DATA)) + (winningValue (mod vrfSample commitTotal)) + ) + (asserts! (has-mined-at-block cityId claimHeight userId) ERR_DID_NOT_MINE) + (asserts! (and (> (get miners blockStats) u0) (> (get commit minerStats) u0)) ERR_NO_MINER_DATA) + (asserts! (not (get claimed blockStats)) ERR_ALREADY_CLAIMED) + (asserts! (and (>= winningValue (get low minerStats)) (<= winningValue (get high minerStats))) ERR_MINER_NOT_WINNER) + (map-set MiningStats + { cityId: cityId, height: claimHeight } + (merge blockStats { claimed: true }) + ) + (map-set Miners + { cityId: cityId, height: claimHeight, userId: userId } + (merge minerStats { winner: true }) + ) + (map-set Winners + { cityId: cityId, height: claimHeight } + userId + ) + (print { + event: "mining-claim", + cityId: cityId, + cityName: cityName, + claimHeight: claimHeight, + userId: userId + }) + (contract-call? .ccd010-core-v2-adapter mint-coinbase cityName tx-sender (get-coinbase-amount cityId claimHeight)) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (get-reward-delay) + (var-get rewardDelay) +) + +(define-read-only (get-mining-stats (cityId uint) (height uint)) + (default-to { miners: u0, amount: u0, claimed: false } + (map-get? MiningStats { cityId: cityId, height: height }) + ) +) + +(define-read-only (has-mined-at-block (cityId uint) (height uint) (userId uint)) + (is-some (map-get? Miners { cityId: cityId, height: height, userId: userId })) +) + +(define-read-only (get-miner (cityId uint) (height uint) (userId uint)) + (default-to { commit: u0, low: u0, high: u0, winner: false } + (map-get? Miners { cityId: cityId, height: height, userId: userId }) + ) +) + +(define-read-only (get-high-value (cityId uint) (height uint)) + (default-to u0 + (map-get? HighValues { cityId: cityId, height: height }) + ) +) + +(define-read-only (get-block-winner (cityId uint) (height uint)) + (map-get? Winners { cityId: cityId, height: height }) +) + +(define-read-only (is-block-winner (cityId uint) (user principal) (claimHeight uint)) + (let + ( + (userId (default-to u0 (contract-call? .ccd003-user-registry get-user-id user))) + (blockStats (get-mining-stats cityId claimHeight)) + (minerStats (get-miner cityId claimHeight userId)) + ;; MAINNET: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 + ;; TESTNET: 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6.citycoin-vrf-v2 + (vrfSample (unwrap! (contract-call? .citycoin-vrf-v2 get-rnd (+ (get-reward-delay) claimHeight)) none)) + (commitTotal (get-high-value cityId claimHeight)) + (winningValue (mod vrfSample commitTotal)) + ) + (if (and (> userId u0) (>= winningValue (get low minerStats)) (<= winningValue (get high minerStats))) + (some { winner: true, claimed: (get claimed blockStats) }) + (some { winner: false, claimed: (get claimed blockStats) }) + ) + ) +) + +(define-read-only (get-coinbase-amount (cityId uint) (height uint)) + (let + ( + (coinbaseInfo (contract-call? .ccd005-city-data get-coinbase-info cityId)) + (thresholds (unwrap! (get thresholds coinbaseInfo) u0)) + (amounts (unwrap! (get amounts coinbaseInfo) u0)) + (details (unwrap! (get details coinbaseInfo) u0)) + (bonusPeriod (get bonus details)) + (cityDetails (unwrap! (contract-call? .ccd005-city-data get-activation-details cityId) u0)) + ) + (asserts! (>= height (get activatedAt cityDetails)) u0) + (asserts! (> height (get cbt1 thresholds)) + (if (<= (- height (get activatedAt cityDetails)) bonusPeriod) + (get cbaBonus amounts) + (get cba1 amounts) + ) + ) + (asserts! (> height (get cbt2 thresholds)) (get cba2 amounts)) + (asserts! (> height (get cbt3 thresholds)) (get cba3 amounts)) + (asserts! (> height (get cbt4 thresholds)) (get cba4 amounts)) + (asserts! (> height (get cbt5 thresholds)) (get cba5 amounts)) + (get cbaDefault amounts) + ) +) + +(define-read-only (is-mining-enabled) + (var-get miningEnabled) +) + +;; PRIVATE FUNCTIONS + +(define-private (mine-block (amount uint) + (return (response + { cityId: uint, userId: uint, height: uint, totalAmount: uint } + uint + ))) + (let + ( + (okReturn (try! return)) + (cityId (get cityId okReturn)) + (userId (get userId okReturn)) + (height (get height okReturn)) + ) + (asserts! (> amount u0) ERR_INVALID_COMMITS) + (let + ( + (blockStats (get-mining-stats cityId height)) + (vrfLowVal (get-high-value cityId height)) + ) + (map-set MiningStats + { cityId: cityId, height: height } + { miners: (+ (get miners blockStats) u1), amount: (+ (get amount blockStats) amount), claimed: false } + ) + (asserts! (map-insert Miners + { cityId: cityId, height: height, userId: userId } + { + commit: amount, + low: (if (> vrfLowVal u0) (+ vrfLowVal u1) u0), + high: (+ vrfLowVal amount), + winner: false + } + ) ERR_ALREADY_MINED) + (map-set HighValues + { cityId: cityId, height: height } + (+ vrfLowVal amount) + ) + ) + (ok (merge okReturn + { height: (+ height u1), totalAmount: (+ (get totalAmount okReturn) amount) } + )) + ) +) diff --git a/contracts/proposals/ccip014-pox-2.clar b/contracts/proposals/ccip014-pox-2.clar new file mode 100644 index 00000000..ffda1146 --- /dev/null +++ b/contracts/proposals/ccip014-pox-2.clar @@ -0,0 +1,5 @@ +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (ok true) +) From 30802fbcece5861a64f6e77c499983dc03a6a7f3 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 4 May 2023 18:03:42 -0700 Subject: [PATCH 02/47] fix: update delegate/revoke to pox-3 --- contracts/extensions/ccd002-treasury-v2.clar | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/ccd002-treasury-v2.clar b/contracts/extensions/ccd002-treasury-v2.clar index 528c6ff7..ea743967 100644 --- a/contracts/extensions/ccd002-treasury-v2.clar +++ b/contracts/extensions/ccd002-treasury-v2.clar @@ -152,8 +152,8 @@ delegate: to, sender: tx-sender }) - ;; MAINNET: 'SP000000000000000000002Q6VF78.pox - (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox delegate-stx maxAmount to none none)) + ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 delegate-stx maxAmount to none none)) success (ok success) err (err (to-uint err)) ) @@ -168,8 +168,8 @@ caller: contract-caller, sender: tx-sender }) - ;; MAINNET: 'SP000000000000000000002Q6VF78.pox - (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox revoke-delegate-stx)) + ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 revoke-delegate-stx)) success (ok success) err (err (to-uint err)) ) From 0fee8c6f8a0468192261bace69e6236859561065 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 4 May 2023 18:04:55 -0700 Subject: [PATCH 03/47] fix: update mining cityinfo call to mining-v2 --- contracts/extensions/ccd006-citycoin-mining-v2.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/extensions/ccd006-citycoin-mining-v2.clar b/contracts/extensions/ccd006-citycoin-mining-v2.clar index 458146e9..139503bd 100644 --- a/contracts/extensions/ccd006-citycoin-mining-v2.clar +++ b/contracts/extensions/ccd006-citycoin-mining-v2.clar @@ -94,7 +94,7 @@ (let ( (cityId (unwrap! (contract-call? .ccd004-city-registry get-city-id cityName) ERR_INVALID_CITY)) - (cityInfo (contract-call? .ccd005-city-data get-city-info cityId "mining")) + (cityInfo (contract-call? .ccd005-city-data get-city-info cityId "mining-v2")) (cityDetails (unwrap! (get details cityInfo) ERR_NO_ACTIVATION_DETAILS)) (cityTreasury (unwrap! (get treasury cityInfo) ERR_INVALID_TREASURY)) (user tx-sender) From c13c5168579ee8995ab23a1e5442e6dc6f5b8ef2 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 4 May 2023 18:24:48 -0700 Subject: [PATCH 04/47] fix: update ccip-014 proposal name and code --- contracts/proposals/ccip014-pox-2.clar | 5 ---- contracts/proposals/ccip014-pox-3.clar | 40 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) delete mode 100644 contracts/proposals/ccip014-pox-2.clar create mode 100644 contracts/proposals/ccip014-pox-3.clar diff --git a/contracts/proposals/ccip014-pox-2.clar b/contracts/proposals/ccip014-pox-2.clar deleted file mode 100644 index ffda1146..00000000 --- a/contracts/proposals/ccip014-pox-2.clar +++ /dev/null @@ -1,5 +0,0 @@ -(impl-trait .proposal-trait.proposal-trait) - -(define-public (execute (sender principal)) - (ok true) -) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar new file mode 100644 index 00000000..80c7fb43 --- /dev/null +++ b/contracts/proposals/ccip014-pox-3.clar @@ -0,0 +1,40 @@ +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (let + ( + (miaBalance (contract-call? .ccd002-treasury-mia-mining get-balance-stx)) + (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) + ) + + ;; enable mining v2 treasuries in the DAO + (try! (contract-call? .base-dao set-extensions + (list + {extension: .ccd002-treasury-mia-mining-v2, enabled: true} + {extension: .ccd002-treasury-nyc-mining-v2, enabled: true} + ) + )) + + ;; allow MIA/NYC in respective treasuries + ;; MAINNET: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 + ;; MAINNET: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 + (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 true)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 set-allowed 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 true)) + + ;; transfer funds to new treasury extensions + (try! (contract-call? .ccd002-treasury-mia-mining withdraw-stx miaBalance .ccd002-treasury-mia-mining-v2)) + (try! (contract-call? .ccd002-treasury-nyc-mining withdraw-stx nycBalance .ccd002-treasury-nyc-mining-v2)) + + ;; delegate stack the STX in the mining treasuries (up to 50M STX each) + ;; MAINNET: TODO + ;; MAINNET: TODO + (try! (contract-call? .ccd002-treasury-mia-mining delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + (try! (contract-call? .ccd002-treasury-nyc-mining delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + + ;; add treasuries to ccd005-city-data + (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining "mining-v2")) + + (ok true) + ) +) From 28e79b4a7fd20a5d666f670b37103492ed49ac73 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 10 May 2023 15:00:14 -0700 Subject: [PATCH 05/47] fix: add mining-v2 and fix ccip014 in toml --- Clarinet.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Clarinet.toml b/Clarinet.toml index 782f8ffb..5191b47e 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -116,6 +116,9 @@ path = "contracts/extensions/ccd005-city-data.clar" [contracts.ccd006-citycoin-mining] path = "contracts/extensions/ccd006-citycoin-mining.clar" +[contracts.ccd006-citycoin-mining-v2] +path = "contracts/extensions/ccd006-citycoin-mining-v2.clar" + [contracts.ccd007-citycoin-stacking] path = "contracts/extensions/ccd007-citycoin-stacking.clar" @@ -140,8 +143,8 @@ path = "contracts/proposals/ccip013-migration.clar" [contracts.ccip013-activation] path = "contracts/proposals/ccip013-activation.clar" -[contracts.ccip014-pox-2] -path = "contracts/proposals/ccip014-pox-2.clar" +[contracts.ccip014-pox-3] +path = "contracts/proposals/ccip014-pox-3.clar" # CITYCOINS PROTOCOL TRAITS From 105afd962dca3a633cc4a15bb1fda9e3624fd54f Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 10 May 2023 15:51:10 -0700 Subject: [PATCH 06/47] fix: add mock pox-3 contract Waiting on clarification for Clarinet settings, still showing error with function replace-at? but should be included with Clarity 2 rules. --- Clarinet.toml | 19 +- contracts/extensions/ccd002-treasury-v2.clar | 6 +- tests/contracts/external/mock-pox-3.clar | 1320 ++++++++++++++++++ 3 files changed, 1336 insertions(+), 9 deletions(-) create mode 100644 tests/contracts/external/mock-pox-3.clar diff --git a/Clarinet.toml b/Clarinet.toml index 5191b47e..fc880be8 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -183,6 +183,9 @@ path = "tests/contracts/external/test-ccext-nft-mia.clar" [contracts.test-ccext-nft-nyc] path = "tests/contracts/external/test-ccext-nft-nyc.clar" +[contracts.mock-pox-3] +path = "tests/contracts/external/mock-pox-3.clar" + # CITYCOINS LEGACY CONTRACTS [contracts.citycoin-core-v2-trait] @@ -386,11 +389,13 @@ path = "tests/contracts/proposals/test-ccd011-stacking-payouts-001.clar" costs_version = 2 parser_version = 2 -[repl.analysis] -passes = ["check_checker"] +# TEMPORARILY DISABLED + +# [repl.analysis] +# passes = ["check_checker"] -[repl.analysis.check_checker] -strict = false -trusted_sender = false -trusted_caller = false -callee_filter = true +# [repl.analysis.check_checker] +# strict = false +# trusted_sender = false +# trusted_caller = false +# callee_filter = true diff --git a/contracts/extensions/ccd002-treasury-v2.clar b/contracts/extensions/ccd002-treasury-v2.clar index ea743967..81696282 100644 --- a/contracts/extensions/ccd002-treasury-v2.clar +++ b/contracts/extensions/ccd002-treasury-v2.clar @@ -153,7 +153,8 @@ sender: tx-sender }) ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 - (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 delegate-stx maxAmount to none none)) + ;; TESTNET: 'ST000000000000000000002AMW42H.pox-3 + (match (as-contract (contract-call? .mock-pox-3 delegate-stx maxAmount to none none)) success (ok success) err (err (to-uint err)) ) @@ -169,7 +170,8 @@ sender: tx-sender }) ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 - (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 revoke-delegate-stx)) + ;; TESTNET: 'ST000000000000000000002AMW42H.pox-3 + (match (as-contract (contract-call? .mock-pox-3 revoke-delegate-stx)) success (ok success) err (err (to-uint err)) ) diff --git a/tests/contracts/external/mock-pox-3.clar b/tests/contracts/external/mock-pox-3.clar new file mode 100644 index 00000000..c4c64820 --- /dev/null +++ b/tests/contracts/external/mock-pox-3.clar @@ -0,0 +1,1320 @@ +;; LAST UPDATED 2023-05-10 15:00:00 GMT-7 +;; Source: https://github.com/stacks-network/stacks-blockchain/blob/feat/epoch-2.4/src/chainstate/stacks/boot/pox-3.clar +;; Last commit hash: 04f6ed29cdd444e28678860270beba62ea7a0d59 + +;; The .pox-3 contract +;; Error codes +(define-constant ERR_STACKING_UNREACHABLE 255) +(define-constant ERR_STACKING_CORRUPTED_STATE 254) +(define-constant ERR_STACKING_INSUFFICIENT_FUNDS 1) +(define-constant ERR_STACKING_INVALID_LOCK_PERIOD 2) +(define-constant ERR_STACKING_ALREADY_STACKED 3) +(define-constant ERR_STACKING_NO_SUCH_PRINCIPAL 4) +(define-constant ERR_STACKING_EXPIRED 5) +(define-constant ERR_STACKING_STX_LOCKED 6) +(define-constant ERR_STACKING_PERMISSION_DENIED 9) +(define-constant ERR_STACKING_THRESHOLD_NOT_MET 11) +(define-constant ERR_STACKING_POX_ADDRESS_IN_USE 12) +(define-constant ERR_STACKING_INVALID_POX_ADDRESS 13) +(define-constant ERR_STACKING_ALREADY_REJECTED 17) +(define-constant ERR_STACKING_INVALID_AMOUNT 18) +(define-constant ERR_NOT_ALLOWED 19) +(define-constant ERR_STACKING_ALREADY_DELEGATED 20) +(define-constant ERR_DELEGATION_EXPIRES_DURING_LOCK 21) +(define-constant ERR_DELEGATION_TOO_MUCH_LOCKED 22) +(define-constant ERR_DELEGATION_POX_ADDR_REQUIRED 23) +(define-constant ERR_INVALID_START_BURN_HEIGHT 24) +(define-constant ERR_NOT_CURRENT_STACKER 25) +(define-constant ERR_STACK_EXTEND_NOT_LOCKED 26) +(define-constant ERR_STACK_INCREASE_NOT_LOCKED 27) +(define-constant ERR_DELEGATION_NO_REWARD_SLOT 28) +(define-constant ERR_DELEGATION_WRONG_REWARD_SLOT 29) +(define-constant ERR_STACKING_IS_DELEGATED 30) +(define-constant ERR_STACKING_NOT_DELEGATED 31) + +;; PoX disabling threshold (a percent) +(define-constant POX_REJECTION_FRACTION u25) + +;; Valid values for burnchain address versions. +;; These first four correspond to address hash modes in Stacks 2.1, +;; and are defined in pox-mainnet.clar and pox-testnet.clar (so they +;; cannot be defined here again). +;; (define-constant ADDRESS_VERSION_P2PKH 0x00) +;; (define-constant ADDRESS_VERSION_P2SH 0x01) +;; (define-constant ADDRESS_VERSION_P2WPKH 0x02) +;; (define-constant ADDRESS_VERSION_P2WSH 0x03) +(define-constant ADDRESS_VERSION_NATIVE_P2WPKH 0x04) +(define-constant ADDRESS_VERSION_NATIVE_P2WSH 0x05) +(define-constant ADDRESS_VERSION_NATIVE_P2TR 0x06) +;; Keep these constants in lock-step with the address version buffs above +;; Maximum value of an address version as a uint +(define-constant MAX_ADDRESS_VERSION u6) +;; Maximum value of an address version that has a 20-byte hashbytes +;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) +;; Maximum value of an address version that has a 32-byte hashbytes +;; (0x05 and 0x06 have 32-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) + +;; Data vars that store a copy of the burnchain configuration. +;; Implemented as data-vars, so that different configurations can be +;; used in e.g. test harnesses. +(define-data-var pox-prepare-cycle-length uint PREPARE_CYCLE_LENGTH) +(define-data-var pox-reward-cycle-length uint REWARD_CYCLE_LENGTH) +(define-data-var pox-rejection-fraction uint POX_REJECTION_FRACTION) +(define-data-var first-burnchain-block-height uint u0) +(define-data-var configured bool false) +(define-data-var first-2-1-reward-cycle uint u0) + +;; This function can only be called once, when it boots up +(define-public (set-burnchain-parameters (first-burn-height uint) + (prepare-cycle-length uint) + (reward-cycle-length uint) + (rejection-fraction uint) + (begin-2-1-reward-cycle uint)) + (begin + (asserts! (not (var-get configured)) (err ERR_NOT_ALLOWED)) + (var-set first-burnchain-block-height first-burn-height) + (var-set pox-prepare-cycle-length prepare-cycle-length) + (var-set pox-reward-cycle-length reward-cycle-length) + (var-set pox-rejection-fraction rejection-fraction) + (var-set first-2-1-reward-cycle begin-2-1-reward-cycle) + (var-set configured true) + (ok true)) +) + +;; The Stacking lock-up state and associated metadata. +;; Records are inserted into this map via `stack-stx`, `delegate-stack-stx`, `stack-extend` +;; `delegate-stack-extend` and burnchain transactions for invoking `stack-stx`, etc. +;; Records will be deleted from this map when auto-unlocks are processed +;; +;; This map de-normalizes some state from the `reward-cycle-pox-address-list` map +;; and the `pox-3` contract tries to keep this state in sync with the reward-cycle +;; state. The major invariants of this `stacking-state` map are: +;; (1) any entry in `reward-cycle-pox-address-list` with `some stacker` points to a real `stacking-state` +;; (2) `stacking-state.reward-set-indexes` matches the index of that `reward-cycle-pox-address-list` +;; (3) all `stacking-state.reward-set-indexes` match the index of their reward cycle entries +;; (4) `stacking-state.pox-addr` matches `reward-cycle-pox-address-list.pox-addr` +;; (5) if set, (len reward-set-indexes) == lock-period +;; (6) (reward-cycle-to-burn-height (+ lock-period first-reward-cycle)) == (get unlock-height (stx-account stacker)) +;; These invariants only hold while `cur-reward-cycle < (+ lock-period first-reward-cycle)` +;; +(define-map stacking-state + { stacker: principal } + { + ;; Description of the underlying burnchain address that will + ;; receive PoX'ed tokens. Translating this into an address + ;; depends on the burnchain being used. When Bitcoin is + ;; the burnchain, this gets translated into a p2pkh, p2sh, + ;; p2wpkh-p2sh, p2wsh-p2sh, p2wpkh, p2wsh, or p2tr UTXO, + ;; depending on the version. The `hashbytes` field *must* be + ;; either 20 bytes or 32 bytes, depending on the output. + pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + ;; how long the uSTX are locked, in reward cycles. + lock-period: uint, + ;; reward cycle when rewards begin + first-reward-cycle: uint, + ;; indexes in each reward-set associated with this user. + ;; these indexes are only valid looking forward from + ;; `first-reward-cycle` (i.e., they do not correspond + ;; to entries in the reward set that may have been from + ;; previous stack-stx calls, or prior to an extend) + reward-set-indexes: (list 12 uint), + ;; principal of the delegate, if stacker has delegated + delegated-to: (optional principal) + } +) + +;; Delegation relationships +(define-map delegation-state + { stacker: principal } + { + amount-ustx: uint, ;; how many uSTX delegated? + delegated-to: principal, ;; who are we delegating? + until-burn-ht: (optional uint), ;; how long does the delegation last? + ;; does the delegate _need_ to use a specific + ;; pox recipient address? + pox-addr: (optional { version: (buff 1), hashbytes: (buff 32) }) + } +) + +;; allowed contract-callers +(define-map allowance-contract-callers + { sender: principal, contract-caller: principal } + { until-burn-ht: (optional uint) }) + +;; How many uSTX are stacked in a given reward cycle. +;; Updated when a new PoX address is registered, or when more STX are granted +;; to it. +(define-map reward-cycle-total-stacked + { reward-cycle: uint } + { total-ustx: uint } +) + +;; Internal map read by the Stacks node to iterate through the list of +;; PoX reward addresses on a per-reward-cycle basis. +(define-map reward-cycle-pox-address-list + { reward-cycle: uint, index: uint } + { + pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + total-ustx: uint, + stacker: (optional principal) + } +) + +(define-map reward-cycle-pox-address-list-len + { reward-cycle: uint } + { len: uint } +) + +;; how much has been locked up for this address before +;; committing? +;; this map allows stackers to stack amounts < minimum +;; by paying the cost of aggregation during the commit +(define-map partial-stacked-by-cycle + { + pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + reward-cycle: uint, + sender: principal + } + { stacked-amount: uint } +) + +;; This is identical to partial-stacked-by-cycle, but its data is never deleted. +;; It is used to preserve data for downstream clients to observe aggregate +;; commits. Each key/value pair in this map is simply the last value of +;; partial-stacked-by-cycle right after it was deleted (so, subsequent calls +;; to the `stack-aggregation-*` functions will overwrite this). +(define-map logged-partial-stacked-by-cycle + { + pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + reward-cycle: uint, + sender: principal + } + { stacked-amount: uint } +) + +;; Amount of uSTX that reject PoX, by reward cycle +(define-map stacking-rejection + { reward-cycle: uint } + { amount: uint } +) + +;; Who rejected in which reward cycle +(define-map stacking-rejectors + { stacker: principal, reward-cycle: uint } + { amount: uint } +) + +;; Getter for stacking-rejectors +(define-read-only (get-pox-rejection (stacker principal) (reward-cycle uint)) + (map-get? stacking-rejectors { stacker: stacker, reward-cycle: reward-cycle })) + +;; Has PoX been rejected in the given reward cycle? +(define-read-only (is-pox-active (reward-cycle uint)) + (let ( + (reject-votes + (default-to + u0 + (get amount (map-get? stacking-rejection { reward-cycle: reward-cycle })))) + ) + ;; (100 * reject-votes) / stx-liquid-supply < pox-rejection-fraction + (< (* u100 reject-votes) + (* (var-get pox-rejection-fraction) stx-liquid-supply))) +) + +;; What's the reward cycle number of the burnchain block height? +;; Will runtime-abort if height is less than the first burnchain block (this is intentional) +(define-read-only (burn-height-to-reward-cycle (height uint)) + (/ (- height (var-get first-burnchain-block-height)) (var-get pox-reward-cycle-length))) + +;; What's the block height at the start of a given reward cycle? +(define-read-only (reward-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) (* cycle (var-get pox-reward-cycle-length)))) + +;; What's the current PoX reward cycle? +(define-read-only (current-pox-reward-cycle) + (burn-height-to-reward-cycle burn-block-height)) + +;; Get the _current_ PoX stacking principal information. If the information +;; is expired, or if there's never been such a stacker, then returns none. +(define-read-only (get-stacker-info (stacker principal)) + (match (map-get? stacking-state { stacker: stacker }) + stacking-info + (if (<= (+ (get first-reward-cycle stacking-info) (get lock-period stacking-info)) (current-pox-reward-cycle)) + ;; present, but lock has expired + none + ;; present, and lock has not expired + (some stacking-info) + ) + ;; no state at all + none + )) + +(define-read-only (check-caller-allowed) + (or (is-eq tx-sender contract-caller) + (let ((caller-allowed + ;; if not in the caller map, return false + (unwrap! (map-get? allowance-contract-callers + { sender: tx-sender, contract-caller: contract-caller }) + false)) + (expires-at + ;; if until-burn-ht not set, then return true (because no expiry) + (unwrap! (get until-burn-ht caller-allowed) true))) + ;; is the caller allowance expired? + (if (>= burn-block-height expires-at) + false + true)))) + +(define-read-only (get-check-delegation (stacker principal)) + (let ((delegation-info (try! (map-get? delegation-state { stacker: stacker })))) + ;; did the existing delegation expire? + (if (match (get until-burn-ht delegation-info) + until-burn-ht (> burn-block-height until-burn-ht) + false) + ;; it expired, return none + none + ;; delegation is active + (some delegation-info)))) + +;; Get the size of the reward set for a reward cycle. +;; Note that this does _not_ return duplicate PoX addresses. +;; Note that this also _will_ return PoX addresses that are beneath +;; the minimum threshold -- i.e. the threshold can increase after insertion. +;; Used internally by the Stacks node, which filters out the entries +;; in this map to select PoX addresses with enough STX. +(define-read-only (get-reward-set-size (reward-cycle uint)) + (default-to + u0 + (get len (map-get? reward-cycle-pox-address-list-len { reward-cycle: reward-cycle })))) + +;; How many rejection votes have we been accumulating for the next block +(define-read-only (next-cycle-rejection-votes) + (default-to + u0 + (get amount (map-get? stacking-rejection { reward-cycle: (+ u1 (current-pox-reward-cycle)) })))) + +;; Add a single PoX address to a single reward cycle. +;; Used to build up a set of per-reward-cycle PoX addresses. +;; No checking will be done -- don't call if this PoX address is already registered in this reward cycle! +;; Returns the index into the reward cycle that the PoX address is stored to +(define-private (append-reward-cycle-pox-addr (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (reward-cycle uint) + (amount-ustx uint) + (stacker (optional principal))) + (let ((sz (get-reward-set-size reward-cycle))) + (map-set reward-cycle-pox-address-list + { reward-cycle: reward-cycle, index: sz } + { pox-addr: pox-addr, total-ustx: amount-ustx, stacker: stacker }) + (map-set reward-cycle-pox-address-list-len + { reward-cycle: reward-cycle } + { len: (+ u1 sz) }) + sz)) + +;; How many uSTX are stacked? +(define-read-only (get-total-ustx-stacked (reward-cycle uint)) + (default-to + u0 + (get total-ustx (map-get? reward-cycle-total-stacked { reward-cycle: reward-cycle }))) +) + +;; Called internally by the node to iterate through the list of PoX addresses in this reward cycle. +;; Returns (optional (tuple (pox-addr <pox-address>) (total-ustx <uint>))) +(define-read-only (get-reward-set-pox-address (reward-cycle uint) (index uint)) + (map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: index })) + +(define-private (fold-unlock-reward-cycle (set-index uint) + (data-res (response { cycle: uint, + first-unlocked-cycle: uint, + stacker: principal + } int))) + (let ((data (try! data-res)) + (cycle (get cycle data)) + (first-unlocked-cycle (get first-unlocked-cycle data))) + ;; if current-cycle hasn't reached first-unlocked-cycle, just continue to next iter + (asserts! (>= cycle first-unlocked-cycle) (ok (merge data { cycle: (+ u1 cycle) }))) + (let ((cycle-entry (unwrap-panic (map-get? reward-cycle-pox-address-list { reward-cycle: cycle, index: set-index }))) + (cycle-entry-u (get stacker cycle-entry)) + (cycle-entry-total-ustx (get total-ustx cycle-entry)) + (cycle-last-entry-ix (- (get len (unwrap-panic (map-get? reward-cycle-pox-address-list-len { reward-cycle: cycle }))) u1))) + (asserts! (is-eq cycle-entry-u (some (get stacker data))) (err ERR_STACKING_CORRUPTED_STATE)) + (if (not (is-eq cycle-last-entry-ix set-index)) + ;; do a "move" if the entry to remove isn't last + (let ((move-entry (unwrap-panic (map-get? reward-cycle-pox-address-list { reward-cycle: cycle, index: cycle-last-entry-ix })))) + (map-set reward-cycle-pox-address-list + { reward-cycle: cycle, index: set-index } + move-entry) + (match (get stacker move-entry) moved-stacker + ;; if the moved entry had an associated stacker, update its state + (let ((moved-state (unwrap-panic (map-get? stacking-state { stacker: moved-stacker }))) + ;; calculate the index into the reward-set-indexes that `cycle` is at + (moved-cycle-index (- cycle (get first-reward-cycle moved-state))) + (moved-reward-list (get reward-set-indexes moved-state)) + ;; reward-set-indexes[moved-cycle-index] = set-index via slice?, append, concat. + (update-list (unwrap-panic (replace-at? moved-reward-list moved-cycle-index set-index)))) + (map-set stacking-state { stacker: moved-stacker } + (merge moved-state { reward-set-indexes: update-list }))) + ;; otherwise, we don't need to update stacking-state after move + true)) + ;; if not moving, just noop + true) + ;; in all cases, we now need to delete the last list entry + (map-delete reward-cycle-pox-address-list { reward-cycle: cycle, index: cycle-last-entry-ix }) + (map-set reward-cycle-pox-address-list-len { reward-cycle: cycle } { len: cycle-last-entry-ix }) + ;; finally, update `reward-cycle-total-stacked` + (map-set reward-cycle-total-stacked { reward-cycle: cycle } + { total-ustx: (- (get total-ustx (unwrap-panic (map-get? reward-cycle-total-stacked { reward-cycle: cycle }))) + cycle-entry-total-ustx) }) + (ok (merge data { cycle: (+ u1 cycle)} ))))) + +;; This method is called by the Stacks block processor directly in order to handle the contract state mutations +;; associated with an early unlock. This can only be invoked by the block processor: it is private, and no methods +;; from this contract invoke it. +(define-private (handle-unlock (user principal) (amount-locked uint) (cycle-to-unlock uint)) + (let ((user-stacking-state (unwrap-panic (map-get? stacking-state { stacker: user }))) + (first-cycle-locked (get first-reward-cycle user-stacking-state)) + (reward-set-indexes (get reward-set-indexes user-stacking-state))) + ;; iterate over each reward set the user is a member of, and remove them from the sets. only apply to reward sets after cycle-to-unlock. + (try! (fold fold-unlock-reward-cycle reward-set-indexes (ok { cycle: first-cycle-locked, first-unlocked-cycle: cycle-to-unlock, stacker: user }))) + ;; Now that we've cleaned up all the reward set entries for the user, delete the user's stacking-state + (map-delete stacking-state { stacker: user }) + (ok true))) + +;; Add a PoX address to the `cycle-index`-th reward cycle, if `cycle-index` is between 0 and the given num-cycles (exclusive). +;; Arguments are given as a tuple, so this function can be (folded ..)'ed onto a list of its arguments. +;; Used by add-pox-addr-to-reward-cycles. +;; No checking is done. +;; The returned tuple is the same as inputted `params`, but the `i` field is incremented if +;; the pox-addr was added to the given cycle. Also, `reward-set-indexes` grows to include all +;; of the `reward-cycle-index` key parts of the `reward-cycle-pox-address-list` which get added by this function. +;; This way, the caller knows which items in a given reward cycle's PoX address list got updated. +(define-private (add-pox-addr-to-ith-reward-cycle (cycle-index uint) (params (tuple + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (reward-set-indexes (list 12 uint)) + (first-reward-cycle uint) + (num-cycles uint) + (stacker (optional principal)) + (amount-ustx uint) + (i uint)))) + (let ((reward-cycle (+ (get first-reward-cycle params) (get i params))) + (num-cycles (get num-cycles params)) + (i (get i params)) + (reward-set-index (if (< i num-cycles) + (let ((total-ustx (get-total-ustx-stacked reward-cycle)) + (reward-index + ;; record how many uSTX this pox-addr will stack for in the given reward cycle + (append-reward-cycle-pox-addr + (get pox-addr params) + reward-cycle + (get amount-ustx params) + (get stacker params) + ))) + ;; update running total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: (+ (get amount-ustx params) total-ustx) }) + (some reward-index)) + none)) + (next-i (if (< i num-cycles) (+ i u1) i))) + { + pox-addr: (get pox-addr params), + first-reward-cycle: (get first-reward-cycle params), + num-cycles: num-cycles, + amount-ustx: (get amount-ustx params), + stacker: (get stacker params), + reward-set-indexes: (match + reward-set-index new (unwrap-panic (as-max-len? (append (get reward-set-indexes params) new) u12)) + (get reward-set-indexes params)), + i: next-i + })) + +;; Add a PoX address to a given sequence of reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-addr-to-reward-cycles (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint) + (stacker principal)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11)) + (results (fold add-pox-addr-to-ith-reward-cycle cycle-indexes + { pox-addr: pox-addr, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, + reward-set-indexes: (list), amount-ustx: amount-ustx, i: u0, stacker: (some stacker) })) + (reward-set-indexes (get reward-set-indexes results))) + ;; For safety, add up the number of times (add-principal-to-ith-reward-cycle) returns 1. + ;; It _should_ be equal to num-cycles. + (asserts! (is-eq num-cycles (get i results)) (err ERR_STACKING_UNREACHABLE)) + (asserts! (is-eq num-cycles (len reward-set-indexes)) (err ERR_STACKING_UNREACHABLE)) + (ok reward-set-indexes))) + +(define-private (add-pox-partial-stacked-to-ith-cycle + (cycle-index uint) + (params { pox-addr: { version: (buff 1), hashbytes: (buff 32) }, + reward-cycle: uint, + num-cycles: uint, + amount-ustx: uint })) + (let ((pox-addr (get pox-addr params)) + (num-cycles (get num-cycles params)) + (reward-cycle (get reward-cycle params)) + (amount-ustx (get amount-ustx params))) + (let ((current-amount + (default-to u0 + (get stacked-amount + (map-get? partial-stacked-by-cycle { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle }))))) + (if (>= cycle-index num-cycles) + ;; do not add to cycles >= cycle-index + false + ;; otherwise, add to the partial-stacked-by-cycle + (map-set partial-stacked-by-cycle + { sender: tx-sender, pox-addr: pox-addr, reward-cycle: reward-cycle } + { stacked-amount: (+ amount-ustx current-amount) })) + ;; produce the next params tuple + { pox-addr: pox-addr, + reward-cycle: (+ u1 reward-cycle), + num-cycles: num-cycles, + amount-ustx: amount-ustx }))) + +;; Add a PoX address to a given sequence of partial reward cycle lists. +;; A PoX address can be added to at most 12 consecutive cycles. +;; No checking is done. +(define-private (add-pox-partial-stacked (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (first-reward-cycle uint) + (num-cycles uint) + (amount-ustx uint)) + (let ((cycle-indexes (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11))) + (fold add-pox-partial-stacked-to-ith-cycle cycle-indexes + { pox-addr: pox-addr, reward-cycle: first-reward-cycle, num-cycles: num-cycles, amount-ustx: amount-ustx }) + true)) + +;; What is the minimum number of uSTX to be stacked in the given reward cycle? +;; Used internally by the Stacks node, and visible publicly. +(define-read-only (get-stacking-minimum) + (/ stx-liquid-supply STACKING_THRESHOLD_25)) + +;; Is the address mode valid for a PoX address? +(define-read-only (check-pox-addr-version (version (buff 1))) + (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION)) + +;; Is this buffer the right length for the given PoX address? +(define-read-only (check-pox-addr-hashbytes (version (buff 1)) (hashbytes (buff 32))) + (if (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION_BUFF_20) + (is-eq (len hashbytes) u20) + (if (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION_BUFF_32) + (is-eq (len hashbytes) u32) + false))) + +;; Is the given lock period valid? +(define-read-only (check-pox-lock-period (lock-period uint)) + (and (>= lock-period MIN_POX_REWARD_CYCLES) + (<= lock-period MAX_POX_REWARD_CYCLES))) + +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (can-stack-stx (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; minimum uSTX must be met + (asserts! (<= (get-stacking-minimum) amount-ustx) + (err ERR_STACKING_THRESHOLD_NOT_MET)) + + (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle num-cycles))) + +;; Evaluate if a participant can stack an amount of STX for a given period. +;; This method is designed as a read-only method so that it can be used as +;; a set of guard conditions and also as a read-only RPC call that can be +;; performed beforehand. +(define-read-only (minimal-can-stack-stx + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (amount-ustx uint) + (first-reward-cycle uint) + (num-cycles uint)) + (begin + ;; amount must be valid + (asserts! (> amount-ustx u0) + (err ERR_STACKING_INVALID_AMOUNT)) + + ;; sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender first-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; address version must be valid + (asserts! (check-pox-addr-version (get version pox-addr)) + (err ERR_STACKING_INVALID_POX_ADDRESS)) + + ;; address hashbytes must be valid for the version + (asserts! (check-pox-addr-hashbytes (get version pox-addr) (get hashbytes pox-addr)) + (err ERR_STACKING_INVALID_POX_ADDRESS)) + + (ok true))) + +;; Revoke contract-caller authorization to call stacking methods +(define-public (disallow-contract-caller (caller principal)) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller })))) + +;; Give a contract-caller authorization to call stacking methods +;; normally, stacking methods may only be invoked by _direct_ transactions +;; (i.e., the tx-sender issues a direct contract-call to the stacking methods) +;; by issuing an allowance, the tx-sender may call through the allowed contract +(define-public (allow-contract-caller (caller principal) (until-burn-ht (optional uint))) + (begin + (asserts! (is-eq tx-sender contract-caller) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-set allowance-contract-callers + { sender: tx-sender, contract-caller: caller } + { until-burn-ht: until-burn-ht })))) + +;; Lock up some uSTX for stacking! Note that the given amount here is in micro-STX (uSTX). +;; The STX will be locked for the given number of reward cycles (lock-period). +;; This is the self-service interface. tx-sender will be the Stacker. +;; +;; * The given stacker cannot currently be stacking. +;; * You will need the minimum uSTX threshold. This will be determined by (get-stacking-minimum) +;; at the time this method is called. +;; * You may need to increase the amount of uSTX locked up later, since the minimum uSTX threshold +;; may increase between reward cycles. +;; * The Stacker will receive rewards in the reward cycle following `start-burn-ht`. +;; Importantly, `start-burn-ht` may not be further into the future than the next reward cycle, +;; and in most cases should be set to the current burn block height. +;; +;; The tokens will unlock and be returned to the Stacker (tx-sender) automatically. +(define-public (stack-stx (amount-ustx uint) + (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) + (start-burn-ht uint) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; ensure that stacking can be performed + (try! (can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked + (let ((reward-set-indexes (try! (add-pox-addr-to-reward-cycles pox-addr first-reward-cycle lock-period amount-ustx tx-sender)))) + ;; add stacker record + (map-set stacking-state + { stacker: tx-sender } + { pox-addr: pox-addr, + reward-set-indexes: reward-set-indexes, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period, + delegated-to: none }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: tx-sender, lock-amount: amount-ustx, unlock-burn-height: (reward-cycle-to-burn-height (+ first-reward-cycle lock-period)) })))) + +(define-public (revoke-delegate-stx) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (ok (map-delete delegation-state { stacker: tx-sender })))) + +;; Delegate to `delegate-to` the ability to stack from a given address. +;; This method _does not_ lock the funds, rather, it allows the delegate +;; to issue the stacking lock. +;; The caller specifies: +;; * amount-ustx: the total amount of ustx the delegate may be allowed to lock +;; * until-burn-ht: an optional burn height at which this delegation expires +;; * pox-addr: an optional address to which any rewards *must* be sent +(define-public (delegate-stx (amount-ustx uint) + (delegate-to principal) + (until-burn-ht (optional uint)) + (pox-addr (optional { version: (buff 1), + hashbytes: (buff 32) }))) + (begin + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender principal must not be stacking + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; pox-addr, if given, must be valid + (match pox-addr + address + (asserts! (check-pox-addr-version (get version address)) + (err ERR_STACKING_INVALID_POX_ADDRESS)) + true) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; add delegation record + (map-set delegation-state + { stacker: tx-sender } + { amount-ustx: amount-ustx, + delegated-to: delegate-to, + until-burn-ht: until-burn-ht, + pox-addr: pox-addr }) + + (ok true))) + +;; Commit partially stacked STX and allocate a new PoX reward address slot. +;; This allows a stacker/delegate to lock fewer STX than the minimal threshold in multiple transactions, +;; so long as: 1. The pox-addr is the same. +;; 2. This "commit" transaction is called _before_ the PoX anchor block. +;; This ensures that each entry in the reward set returned to the stacks-node is greater than the threshold, +;; but does not require it be all locked up within a single transaction +;; +;; Returns (ok uint) on success, where the given uint is the reward address's index in the list of reward +;; addresses allocated in this reward cycle. This index can then be passed to `stack-aggregation-increase` +;; to later increment the STX this PoX address represents, in amounts less than the stacking minimum. +;; +;; *New in Stacks 2.1.* +(define-private (inner-stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (reward-cycle uint)) + (let ((partial-stacked + ;; fetch the partial commitments + (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (err ERR_STACKING_NO_SUCH_PRINCIPAL)))) + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + (let ((amount-ustx (get stacked-amount partial-stacked))) + (try! (can-stack-stx pox-addr amount-ustx reward-cycle u1)) + ;; Add the pox addr to the reward cycle, and extract the index of the PoX address + ;; so the delegator can later use it to call stack-aggregation-increase. + (let ((add-pox-addr-info + (add-pox-addr-to-ith-reward-cycle + u0 + { pox-addr: pox-addr, + first-reward-cycle: reward-cycle, + num-cycles: u1, + reward-set-indexes: (list), + stacker: none, + amount-ustx: amount-ustx, + i: u0 })) + (pox-addr-index (unwrap-panic + (element-at (get reward-set-indexes add-pox-addr-info) u0)))) + + ;; don't update the stacking-state map, + ;; because it _already has_ this stacker's state + ;; don't lock the STX, because the STX is already locked + ;; + ;; clear the partial-stacked state, and log it + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (map-set logged-partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle } partial-stacked) + (ok pox-addr-index))))) + +;; Legacy interface for stack-aggregation-commit. +;; Wraps inner-stack-aggregation-commit. See its docstring for details. +;; Returns (ok true) on success +;; Returns (err ...) on failure. +(define-public (stack-aggregation-commit (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (reward-cycle uint)) + (match (inner-stack-aggregation-commit pox-addr reward-cycle) + pox-addr-index (ok true) + commit-err (err commit-err))) + +;; Public interface to `inner-stack-aggregation-commit`. See its documentation for details. +;; *New in Stacks 2.1.* +(define-public (stack-aggregation-commit-indexed (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (reward-cycle uint)) + (inner-stack-aggregation-commit pox-addr reward-cycle)) + +;; Commit partially stacked STX to a PoX address which has already received some STX (more than the Stacking min). +;; This allows a delegator to lock up marginally more STX from new delegates, even if they collectively do not +;; exceed the Stacking minimum, so long as the target PoX address already represents at least as many STX as the +;; Stacking minimum. +;; +;; The `reward-cycle-index` is emitted as a contract event from `stack-aggregation-commit` when the initial STX are +;; locked up by this delegator. It must be passed here to add more STX behind this PoX address. If the delegator +;; called `stack-aggregation-commit` multiple times for the same PoX address, then any such `reward-cycle-index` will +;; work here. +;; +;; *New in Stacks 2.1* +;; +(define-public (stack-aggregation-increase (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (reward-cycle uint) + (reward-cycle-index uint)) + (let ((partial-stacked + ;; fetch the partial commitments + (unwrap! (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (err ERR_STACKING_NO_SUCH_PRINCIPAL)))) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; reward-cycle must be in the future + (asserts! (> reward-cycle (current-pox-reward-cycle)) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + (let ((amount-ustx (get stacked-amount partial-stacked)) + ;; reward-cycle must point to an existing record in reward-cycle-total-stacked + ;; infallible; getting something from partial-stacked-by-cycle succeeded so this must succeed + (existing-total (unwrap-panic (map-get? reward-cycle-total-stacked { reward-cycle: reward-cycle }))) + ;; reward-cycle and reward-cycle-index must point to an existing record in reward-cycle-pox-address-list + (existing-entry (unwrap! (map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: reward-cycle-index }) + (err ERR_DELEGATION_NO_REWARD_SLOT))) + (increased-ustx (+ (get total-ustx existing-entry) amount-ustx)) + (total-ustx (+ (get total-ustx existing-total) amount-ustx))) + + ;; must be stackable + (try! (minimal-can-stack-stx pox-addr total-ustx reward-cycle u1)) + + ;; new total must exceed the stacking minimum + (asserts! (<= (get-stacking-minimum) total-ustx) + (err ERR_STACKING_THRESHOLD_NOT_MET)) + + ;; there must *not* be a stacker entry (since this is a delegator) + (asserts! (is-none (get stacker existing-entry)) + (err ERR_DELEGATION_WRONG_REWARD_SLOT)) + + ;; the given PoX address must match the one on record + (asserts! (is-eq pox-addr (get pox-addr existing-entry)) + (err ERR_DELEGATION_WRONG_REWARD_SLOT)) + + ;; update the pox-address list -- bump the total-ustx + (map-set reward-cycle-pox-address-list + { reward-cycle: reward-cycle, index: reward-cycle-index } + { pox-addr: pox-addr, + total-ustx: increased-ustx, + stacker: none }) + + ;; update the total ustx in this cycle + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: total-ustx }) + + ;; don't update the stacking-state map, + ;; because it _already has_ this stacker's state + ;; don't lock the STX, because the STX is already locked + ;; + ;; clear the partial-stacked state, and log it + (map-delete partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle }) + (map-set logged-partial-stacked-by-cycle { pox-addr: pox-addr, sender: tx-sender, reward-cycle: reward-cycle } partial-stacked) + (ok true)))) + +;; As a delegate, stack the given principal's STX using partial-stacked-by-cycle +;; Once the delegate has stacked > minimum, the delegate should call stack-aggregation-commit +(define-public (delegate-stack-stx (stacker principal) + (amount-ustx uint) + (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (start-burn-ht uint) + (lock-period uint)) + ;; this stacker's first reward cycle is the _next_ reward cycle + (let ((first-reward-cycle (+ u1 (current-pox-reward-cycle))) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) + (unlock-burn-height (reward-cycle-to-burn-height (+ (current-pox-reward-cycle) u1 lock-period)))) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + (err ERR_INVALID_START_BURN_HEIGHT)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must have delegated to the caller + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) + ;; must have delegated to tx-sender + (asserts! (is-eq (get delegated-to delegation-info) tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; must have delegated enough stx + (asserts! (>= (get amount-ustx delegation-info) amount-ustx) + (err ERR_DELEGATION_TOO_MUCH_LOCKED)) + ;; if pox-addr is set, must be equal to pox-addr + (asserts! (match (get pox-addr delegation-info) + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + (err ERR_DELEGATION_POX_ADDR_REQUIRED)) + ;; delegation must not expire before lock period + (asserts! (match (get until-burn-ht delegation-info) + until-burn-ht (>= until-burn-ht + unlock-burn-height) + true) + (err ERR_DELEGATION_EXPIRES_DURING_LOCK))) + + ;; stacker principal must not be stacking + (asserts! (is-none (get-stacker-info stacker)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance stacker) amount-ustx) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; ensure that stacking can be performed + (try! (minimal-can-stack-stx pox-addr amount-ustx first-reward-cycle lock-period)) + + ;; register the PoX address with the amount stacked via partial stacking + ;; before it can be included in the reward set, this must be committed! + (add-pox-partial-stacked pox-addr first-reward-cycle lock-period amount-ustx) + + ;; add stacker record + (map-set stacking-state + { stacker: stacker } + { pox-addr: pox-addr, + first-reward-cycle: first-reward-cycle, + reward-set-indexes: (list), + lock-period: lock-period, + delegated-to: (some tx-sender) }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: stacker, + lock-amount: amount-ustx, + unlock-burn-height: unlock-burn-height }))) + +;; Reject Stacking for this reward cycle. +;; tx-sender votes all its uSTX for rejection. +;; Note that unlike PoX, rejecting PoX does not lock the tx-sender's +;; tokens. PoX rejection acts like a coin vote. +(define-public (reject-pox) + (let ( + (balance (stx-get-balance tx-sender)) + (vote-reward-cycle (+ u1 (current-pox-reward-cycle))) + ) + + ;; tx-sender principal must not have rejected in this upcoming reward cycle + (asserts! (is-none (get-pox-rejection tx-sender vote-reward-cycle)) + (err ERR_STACKING_ALREADY_REJECTED)) + + ;; tx-sender can't be a stacker + (asserts! (is-none (get-stacker-info tx-sender)) + (err ERR_STACKING_ALREADY_STACKED)) + + ;; vote for rejection + (map-set stacking-rejection + { reward-cycle: vote-reward-cycle } + { amount: (+ (next-cycle-rejection-votes) balance) } + ) + + ;; mark voted + (map-set stacking-rejectors + { stacker: tx-sender, reward-cycle: vote-reward-cycle } + { amount: balance } + ) + + (ok true)) +) + +;; Used for PoX parameters discovery +(define-read-only (get-pox-info) + (ok { + min-amount-ustx: (get-stacking-minimum), + reward-cycle-id: (current-pox-reward-cycle), + prepare-cycle-length: (var-get pox-prepare-cycle-length), + first-burnchain-block-height: (var-get first-burnchain-block-height), + reward-cycle-length: (var-get pox-reward-cycle-length), + rejection-fraction: (var-get pox-rejection-fraction), + current-rejection-votes: (next-cycle-rejection-votes), + total-liquid-supply-ustx: stx-liquid-supply, + }) +) + +;; Update the number of stacked STX in a given reward cycle entry. +;; `reward-cycle-index` is the index into the `reward-cycle-pox-address-list` map for a given reward cycle number. +;; `updates`, if `(some ..)`, encodes which PoX reward cycle entry (if any) gets updated. In particular, it must have +;; `(some stacker)` as the listed stacker, and must be an upcoming reward cycle. +(define-private (increase-reward-cycle-entry + (reward-cycle-index uint) + (updates (optional { first-cycle: uint, reward-cycle: uint, stacker: principal, add-amount: uint }))) + (let ((data (try! updates)) + (first-cycle (get first-cycle data)) + (reward-cycle (get reward-cycle data))) + (if (> first-cycle reward-cycle) + ;; not at first cycle to process yet + (some { first-cycle: first-cycle, reward-cycle: (+ u1 reward-cycle), stacker: (get stacker data), add-amount: (get add-amount data) }) + (let ((existing-entry (unwrap-panic (map-get? reward-cycle-pox-address-list { reward-cycle: reward-cycle, index: reward-cycle-index }))) + (existing-total (unwrap-panic (map-get? reward-cycle-total-stacked { reward-cycle: reward-cycle }))) + (add-amount (get add-amount data)) + (total-ustx (+ (get total-ustx existing-total) add-amount))) + ;; stacker must match + (asserts! (is-eq (get stacker existing-entry) (some (get stacker data))) none) + ;; update the pox-address list + (map-set reward-cycle-pox-address-list + { reward-cycle: reward-cycle, index: reward-cycle-index } + { pox-addr: (get pox-addr existing-entry), + ;; This addresses the bug in pox-2 (see SIP-022) + total-ustx: (+ (get total-ustx existing-entry) add-amount), + stacker: (some (get stacker data)) }) + ;; update the total + (map-set reward-cycle-total-stacked + { reward-cycle: reward-cycle } + { total-ustx: total-ustx }) + (some { first-cycle: first-cycle, + reward-cycle: (+ u1 reward-cycle), + stacker: (get stacker data), + add-amount: (get add-amount data) }))))) + +;; Increase the number of STX locked. +;; *New in Stacks 2.1* +;; This method locks up an additional amount of STX from `tx-sender`'s, indicated +;; by `increase-by`. The `tx-sender` must already be Stacking. +(define-public (stack-increase (increase-by uint)) + (let ((stacker-info (stx-account tx-sender)) + (amount-stacked (get locked stacker-info)) + (amount-unlocked (get unlocked stacker-info)) + (unlock-height (get unlock-height stacker-info)) + (cur-cycle (current-pox-reward-cycle)) + (first-increased-cycle (+ cur-cycle u1)) + (stacker-state (unwrap! (map-get? stacking-state + { stacker: tx-sender }) + (err ERR_STACK_INCREASE_NOT_LOCKED)))) + ;; tx-sender must be currently locked + (asserts! (> amount-stacked u0) + (err ERR_STACK_INCREASE_NOT_LOCKED)) + ;; must be called with positive `increase-by` + (asserts! (>= increase-by u1) + (err ERR_STACKING_INVALID_AMOUNT)) + ;; stacker must have enough stx to lock + (asserts! (>= amount-unlocked increase-by) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; stacker must be directly stacking + (asserts! (> (len (get reward-set-indexes stacker-state)) u0) + (err ERR_STACKING_IS_DELEGATED)) + ;; stacker must not be delegating + (asserts! (is-none (get delegated-to stacker-state)) + (err ERR_STACKING_IS_DELEGATED)) + ;; update reward cycle amounts + (asserts! (is-some (fold increase-reward-cycle-entry + (get reward-set-indexes stacker-state) + (some { first-cycle: first-increased-cycle, + reward-cycle: (get first-reward-cycle stacker-state), + stacker: tx-sender, + add-amount: increase-by }))) + (err ERR_STACKING_UNREACHABLE)) + ;; NOTE: stacking-state map is unchanged: it does not track amount-stacked in PoX-3 + (ok { stacker: tx-sender, total-locked: (+ amount-stacked increase-by)}))) + +;; Extend an active Stacking lock. +;; *New in Stacks 2.1* +;; This method extends the `tx-sender`'s current lockup for an additional `extend-count` +;; and associates `pox-addr` with the rewards +(define-public (stack-extend (extend-count uint) + (pox-addr { version: (buff 1), hashbytes: (buff 32) })) + (let ((stacker-info (stx-account tx-sender)) + ;; to extend, there must already be an etry in the stacking-state + (stacker-state (unwrap! (get-stacker-info tx-sender) (err ERR_STACK_EXTEND_NOT_LOCKED))) + (amount-ustx (get locked stacker-info)) + (unlock-height (get unlock-height stacker-info)) + (cur-cycle (current-pox-reward-cycle)) + ;; first-extend-cycle will be the cycle in which tx-sender *would have* unlocked + (first-extend-cycle (burn-height-to-reward-cycle unlock-height)) + ;; new first cycle should be max(cur-cycle, stacker-state.first-reward-cycle) + (cur-first-reward-cycle (get first-reward-cycle stacker-state)) + (first-reward-cycle (if (> cur-cycle cur-first-reward-cycle) cur-cycle cur-first-reward-cycle))) + + ;; must be called with positive extend-count + (asserts! (>= extend-count u1) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; stacker must be directly stacking + (asserts! (> (len (get reward-set-indexes stacker-state)) u0) + (err ERR_STACKING_IS_DELEGATED)) + + ;; stacker must not be delegating + (asserts! (is-none (get delegated-to stacker-state)) + (err ERR_STACKING_IS_DELEGATED)) + + ;; TODO: add more assertions to sanity check the `stacker-info` values with + ;; the `stacker-state` values + + (let ((last-extend-cycle (- (+ first-extend-cycle extend-count) u1)) + (lock-period (+ u1 (- last-extend-cycle first-reward-cycle))) + (new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle)))) + + ;; first cycle must be after the current cycle + (asserts! (> first-extend-cycle cur-cycle) (err ERR_STACKING_INVALID_LOCK_PERIOD)) + ;; lock period must be positive + (asserts! (> lock-period u0) (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; tx-sender must be locked + (asserts! (> amount-ustx u0) + (err ERR_STACK_EXTEND_NOT_LOCKED)) + + ;; tx-sender must not be delegating + (asserts! (is-none (get-check-delegation tx-sender)) + (err ERR_STACKING_ALREADY_DELEGATED)) + + ;; standard can-stack-stx checks + (try! (can-stack-stx pox-addr amount-ustx first-extend-cycle lock-period)) + + ;; register the PoX address with the amount stacked + ;; for the new cycles + (let ((extended-reward-set-indexes (try! (add-pox-addr-to-reward-cycles pox-addr first-extend-cycle extend-count amount-ustx tx-sender))) + (reward-set-indexes + ;; use the active stacker state and extend the existing reward-set-indexes + (let ((cur-cycle-index (- first-reward-cycle (get first-reward-cycle stacker-state))) + (old-indexes (get reward-set-indexes stacker-state)) + ;; build index list by taking the old-indexes starting from cur cycle + ;; and adding the new indexes to it. this way, the index is valid starting from the current cycle + (new-list (concat (default-to (list) (slice? old-indexes cur-cycle-index (len old-indexes))) + extended-reward-set-indexes))) + (unwrap-panic (as-max-len? new-list u12))))) + ;; update stacker record + (map-set stacking-state + { stacker: tx-sender } + { pox-addr: pox-addr, + reward-set-indexes: reward-set-indexes, + first-reward-cycle: first-reward-cycle, + lock-period: lock-period, + delegated-to: none }) + + ;; return lock-up information + (ok { stacker: tx-sender, unlock-burn-height: new-unlock-ht }))))) + +;; As a delegator, increase an active Stacking lock, issuing a "partial commitment" for the +;; increased cycles. +;; *New in Stacks 2.1* +;; This method increases `stacker`'s current lockup and partially commits the additional +;; STX to `pox-addr` +(define-public (delegate-stack-increase + (stacker principal) + (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (increase-by uint)) + (let ((stacker-info (stx-account stacker)) + (existing-lock (get locked stacker-info)) + (available-stx (get unlocked stacker-info)) + (unlock-height (get unlock-height stacker-info))) + + ;; must be called with positive `increase-by` + (asserts! (>= increase-by u1) + (err ERR_STACKING_INVALID_AMOUNT)) + + (let ((unlock-in-cycle (burn-height-to-reward-cycle unlock-height)) + (cur-cycle (current-pox-reward-cycle)) + (first-increase-cycle (+ cur-cycle u1)) + (last-increase-cycle (- unlock-in-cycle u1)) + (cycle-count (try! (if (<= first-increase-cycle last-increase-cycle) + (ok (+ u1 (- last-increase-cycle first-increase-cycle))) + (err ERR_STACKING_INVALID_LOCK_PERIOD)))) + (new-total-locked (+ increase-by existing-lock)) + (stacker-state + (unwrap! (map-get? stacking-state { stacker: stacker }) + (err ERR_STACK_INCREASE_NOT_LOCKED)))) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must not be directly stacking + (asserts! (is-eq (len (get reward-set-indexes stacker-state)) u0) + (err ERR_STACKING_NOT_DELEGATED)) + + ;; stacker must be delegated to tx-sender + (asserts! (is-eq (unwrap! (get delegated-to stacker-state) + (err ERR_STACKING_NOT_DELEGATED)) + tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must be currently locked + (asserts! (> existing-lock u0) + (err ERR_STACK_INCREASE_NOT_LOCKED)) + + ;; stacker must have enough stx to lock + (asserts! (>= available-stx increase-by) + (err ERR_STACKING_INSUFFICIENT_FUNDS)) + + ;; stacker must have delegated to the caller + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED))) + (delegated-to (get delegated-to delegation-info)) + (delegated-amount (get amount-ustx delegation-info)) + (delegated-pox-addr (get pox-addr delegation-info)) + (delegated-until (get until-burn-ht delegation-info))) + ;; must have delegated to tx-sender + (asserts! (is-eq delegated-to tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; must have delegated enough stx + (asserts! (>= delegated-amount new-total-locked) + (err ERR_DELEGATION_TOO_MUCH_LOCKED)) + ;; if pox-addr is set, must be equal to pox-addr + (asserts! (match delegated-pox-addr + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + (err ERR_DELEGATION_POX_ADDR_REQUIRED)) + ;; delegation must not expire before lock period + (asserts! (match delegated-until + until-burn-ht + (>= until-burn-ht unlock-height) + true) + (err ERR_DELEGATION_EXPIRES_DURING_LOCK))) + + ;; delegate stacking does minimal-can-stack-stx + (try! (minimal-can-stack-stx pox-addr new-total-locked first-increase-cycle (+ u1 (- last-increase-cycle first-increase-cycle)))) + + ;; register the PoX address with the amount stacked via partial stacking + ;; before it can be included in the reward set, this must be committed! + (add-pox-partial-stacked pox-addr first-increase-cycle cycle-count increase-by) + + ;; stacking-state is unchanged, so no need to update + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: stacker, total-locked: new-total-locked})))) + +;; As a delegator, extend an active stacking lock, issuing a "partial commitment" for the +;; extended-to cycles. +;; *New in Stacks 2.1* +;; This method extends `stacker`'s current lockup for an additional `extend-count` +;; and partially commits those new cycles to `pox-addr` +(define-public (delegate-stack-extend + (stacker principal) + (pox-addr { version: (buff 1), hashbytes: (buff 32) }) + (extend-count uint)) + (let ((stacker-info (stx-account stacker)) + ;; to extend, there must already be an entry in the stacking-state + (stacker-state (unwrap! (get-stacker-info stacker) (err ERR_STACK_EXTEND_NOT_LOCKED))) + (amount-ustx (get locked stacker-info)) + (unlock-height (get unlock-height stacker-info)) + ;; first-extend-cycle will be the cycle in which tx-sender *would have* unlocked + (first-extend-cycle (burn-height-to-reward-cycle unlock-height)) + (cur-cycle (current-pox-reward-cycle)) + ;; new first cycle should be max(cur-cycle, stacker-state.first-reward-cycle) + (cur-first-reward-cycle (get first-reward-cycle stacker-state)) + (first-reward-cycle (if (> cur-cycle cur-first-reward-cycle) cur-cycle cur-first-reward-cycle))) + + ;; must be called with positive extend-count + (asserts! (>= extend-count u1) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + (let ((last-extend-cycle (- (+ first-extend-cycle extend-count) u1)) + (lock-period (+ u1 (- last-extend-cycle first-reward-cycle))) + (new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle)))) + + ;; first cycle must be after the current cycle + (asserts! (> first-extend-cycle cur-cycle) (err ERR_STACKING_INVALID_LOCK_PERIOD)) + ;; lock period must be positive + (asserts! (> lock-period u0) (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (asserts! (check-caller-allowed) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; stacker must not be directly stacking + (asserts! (is-eq (len (get reward-set-indexes stacker-state)) u0) + (err ERR_STACKING_NOT_DELEGATED)) + + ;; stacker must be delegated to tx-sender + (asserts! (is-eq (unwrap! (get delegated-to stacker-state) + (err ERR_STACKING_NOT_DELEGATED)) + tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + + ;; check valid lock period + (asserts! (check-pox-lock-period lock-period) + (err ERR_STACKING_INVALID_LOCK_PERIOD)) + + ;; stacker must be currently locked + (asserts! (> amount-ustx u0) + (err ERR_STACK_EXTEND_NOT_LOCKED)) + + ;; stacker must have delegated to the caller + (let ((delegation-info (unwrap! (get-check-delegation stacker) (err ERR_STACKING_PERMISSION_DENIED)))) + ;; must have delegated to tx-sender + (asserts! (is-eq (get delegated-to delegation-info) tx-sender) + (err ERR_STACKING_PERMISSION_DENIED)) + ;; must have delegated enough stx + (asserts! (>= (get amount-ustx delegation-info) amount-ustx) + (err ERR_DELEGATION_TOO_MUCH_LOCKED)) + ;; if pox-addr is set, must be equal to pox-addr + (asserts! (match (get pox-addr delegation-info) + specified-pox-addr (is-eq pox-addr specified-pox-addr) + true) + (err ERR_DELEGATION_POX_ADDR_REQUIRED)) + ;; delegation must not expire before lock period + (asserts! (match (get until-burn-ht delegation-info) + until-burn-ht (>= until-burn-ht + new-unlock-ht) + true) + (err ERR_DELEGATION_EXPIRES_DURING_LOCK))) + + ;; delegate stacking does minimal-can-stack-stx + (try! (minimal-can-stack-stx pox-addr amount-ustx first-extend-cycle lock-period)) + + ;; register the PoX address with the amount stacked via partial stacking + ;; before it can be included in the reward set, this must be committed! + (add-pox-partial-stacked pox-addr first-extend-cycle extend-count amount-ustx) + + (map-set stacking-state + { stacker: stacker } + { pox-addr: pox-addr, + reward-set-indexes: (list), + first-reward-cycle: first-reward-cycle, + lock-period: lock-period, + delegated-to: (some tx-sender) }) + + ;; return the lock-up information, so the node can actually carry out the lock. + (ok { stacker: stacker, + unlock-burn-height: new-unlock-ht })))) + +;; Get the _current_ PoX stacking delegation information for a stacker. If the information +;; is expired, or if there's never been such a stacker, then returns none. +;; *New in Stacks 2.1* +(define-read-only (get-delegation-info (stacker principal)) + (get-check-delegation stacker) +) + +;; Get the burn height at which a particular contract is allowed to stack for a particular principal. +;; *New in Stacks 2.1* +;; Returns (some (some X)) if X is the burn height at which the allowance terminates +;; Returns (some none) if the caller is allowed indefinitely +;; Returns none if there is no allowance record +(define-read-only (get-allowance-contract-callers (sender principal) (calling-contract principal)) + (map-get? allowance-contract-callers { sender: sender, contract-caller: calling-contract }) +) + +;; How many PoX addresses in this reward cycle? +;; *New in Stacks 2.1* +(define-read-only (get-num-reward-set-pox-addresses (reward-cycle uint)) + (match (map-get? reward-cycle-pox-address-list-len { reward-cycle: reward-cycle }) + num-addrs + (get len num-addrs) + u0 + ) +) + +;; How many uSTX have been locked up for this address so far, before the delegator commits them? +;; *New in Stacks 2.1* +(define-read-only (get-partial-stacked-by-cycle (pox-addr { version: (buff 1), hashbytes: (buff 32) }) (reward-cycle uint) (sender principal)) + (map-get? partial-stacked-by-cycle { pox-addr: pox-addr, reward-cycle: reward-cycle, sender: sender }) +) + +;; How many uSTX have voted to reject PoX in a given reward cycle? +;; *New in Stacks 2.1* +(define-read-only (get-total-pox-rejection (reward-cycle uint)) + (match (map-get? stacking-rejection { reward-cycle: reward-cycle }) + rejected + (get amount rejected) + u0 + ) +) \ No newline at end of file From 295c640d0352677fe0867a23e6816f9def53496c Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 12 May 2023 15:36:11 -0700 Subject: [PATCH 07/47] fix: add mining shutdown to ccip-014 Also fixes Clarinet.toml to specify correct epoch / clarity version for new contracts. --- Clarinet.toml | 4 ++++ contracts/proposals/ccip014-pox-3.clar | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Clarinet.toml b/Clarinet.toml index fc880be8..41f0da8e 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -145,6 +145,8 @@ path = "contracts/proposals/ccip013-activation.clar" [contracts.ccip014-pox-3] path = "contracts/proposals/ccip014-pox-3.clar" +clarity_version = 2 +epoch = 2.1 # CITYCOINS PROTOCOL TRAITS @@ -185,6 +187,8 @@ path = "tests/contracts/external/test-ccext-nft-nyc.clar" [contracts.mock-pox-3] path = "tests/contracts/external/mock-pox-3.clar" +clarity_version = 2 +epoch = 2.1 # CITYCOINS LEGACY CONTRACTS diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 80c7fb43..0aa8a8fb 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -35,6 +35,9 @@ (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining "mining-v2")) (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining "mining-v2")) + ;; disable original mining contract + (try! (contract-call? (contract-call? .ccd006-citycoin-mining set-mining-enabled false))) + (ok true) ) ) From db5bcb103ebbf6badfeb17df2143ecc3730f5c09 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 15 May 2023 17:32:44 -0700 Subject: [PATCH 08/47] fix: add mainnet/testnet constants --- tests/contracts/external/mock-pox-3.clar | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/contracts/external/mock-pox-3.clar b/tests/contracts/external/mock-pox-3.clar index c4c64820..3e36aa7d 100644 --- a/tests/contracts/external/mock-pox-3.clar +++ b/tests/contracts/external/mock-pox-3.clar @@ -2,6 +2,42 @@ ;; Source: https://github.com/stacks-network/stacks-blockchain/blob/feat/epoch-2.4/src/chainstate/stacks/boot/pox-3.clar ;; Last commit hash: 04f6ed29cdd444e28678860270beba62ea7a0d59 +;; Concantenated mainnet constants +;; Min/max number of reward cycles uSTX can be locked for +;; (define-constant MIN_POX_REWARD_CYCLES u1) +;; (define-constant MAX_POX_REWARD_CYCLES u12) +;; Default length of the PoX registration window, in burnchain blocks. +;; (define-constant PREPARE_CYCLE_LENGTH u100) +;; Default length of the PoX reward cycle, in burnchain blocks. +;; (define-constant REWARD_CYCLE_LENGTH u2100) +;; Valid values for burnchain address versions. +;; These correspond to address hash modes in Stacks 2.0. +;; (define-constant ADDRESS_VERSION_P2PKH 0x00) +;; (define-constant ADDRESS_VERSION_P2SH 0x01) +;; (define-constant ADDRESS_VERSION_P2WPKH 0x02) +;; (define-constant ADDRESS_VERSION_P2WSH 0x03) +;; Stacking thresholds +;; (define-constant STACKING_THRESHOLD_25 u20000) +;; (define-constant STACKING_THRESHOLD_100 u5000) + +;; Concantenated testnet constants +;; Min/max number of reward cycles uSTX can be locked for +(define-constant MIN_POX_REWARD_CYCLES u1) +(define-constant MAX_POX_REWARD_CYCLES u12) +;; Default length of the PoX registration window, in burnchain blocks. +(define-constant PREPARE_CYCLE_LENGTH u50) +;; Default length of the PoX reward cycle, in burnchain blocks. +(define-constant REWARD_CYCLE_LENGTH u1050) +;; Valid values for burnchain address versions. +;; These correspond to address hash modes in Stacks 2.0. +(define-constant ADDRESS_VERSION_P2PKH 0x00) +(define-constant ADDRESS_VERSION_P2SH 0x01) +(define-constant ADDRESS_VERSION_P2WPKH 0x02) +(define-constant ADDRESS_VERSION_P2WSH 0x03) +;; Stacking thresholds +(define-constant STACKING_THRESHOLD_25 u8000) +(define-constant STACKING_THRESHOLD_100 u2000) + ;; The .pox-3 contract ;; Error codes (define-constant ERR_STACKING_UNREACHABLE 255) From c7d5d1d02ae30424231f997d355d9903d299b5c8 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 15 May 2023 17:44:57 -0700 Subject: [PATCH 09/47] fix: update to use testnet pox3 and fix check errors This requires a custom version of Clarinet built from the feat/devnet-epoch-2.2 branch of clarinet, as well as the feat/2.4-clarity-wasm branch of stacks-blockchain. The link between the two is relative, so the clarinet/ and stacks-blockchain/ directories should be at the same level. --- contracts/extensions/ccd002-treasury-v2.clar | 4 ++-- contracts/proposals/ccip014-pox-3.clar | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/contracts/extensions/ccd002-treasury-v2.clar b/contracts/extensions/ccd002-treasury-v2.clar index 81696282..f3475a81 100644 --- a/contracts/extensions/ccd002-treasury-v2.clar +++ b/contracts/extensions/ccd002-treasury-v2.clar @@ -154,7 +154,7 @@ }) ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 ;; TESTNET: 'ST000000000000000000002AMW42H.pox-3 - (match (as-contract (contract-call? .mock-pox-3 delegate-stx maxAmount to none none)) + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 delegate-stx maxAmount to none none)) success (ok success) err (err (to-uint err)) ) @@ -171,7 +171,7 @@ }) ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 ;; TESTNET: 'ST000000000000000000002AMW42H.pox-3 - (match (as-contract (contract-call? .mock-pox-3 revoke-delegate-stx)) + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 revoke-delegate-stx)) success (ok success) err (err (to-uint err)) ) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 0aa8a8fb..24999993 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -1,8 +1,12 @@ (impl-trait .proposal-trait.proposal-trait) +(define-constant ERR_PANIC (err u500)) + (define-public (execute (sender principal)) (let ( + (miaId (unwrap! (contract-call? .ccd004-city-registry get-city-id "mia") ERR_PANIC)) + (nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) (miaBalance (contract-call? .ccd002-treasury-mia-mining get-balance-stx)) (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) ) @@ -14,13 +18,13 @@ {extension: .ccd002-treasury-nyc-mining-v2, enabled: true} ) )) - + ;; allow MIA/NYC in respective treasuries ;; MAINNET: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 ;; MAINNET: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 true)) (try! (contract-call? .ccd002-treasury-nyc-mining-v2 set-allowed 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 true)) - + ;; transfer funds to new treasury extensions (try! (contract-call? .ccd002-treasury-mia-mining withdraw-stx miaBalance .ccd002-treasury-mia-mining-v2)) (try! (contract-call? .ccd002-treasury-nyc-mining withdraw-stx nycBalance .ccd002-treasury-nyc-mining-v2)) @@ -28,15 +32,15 @@ ;; delegate stack the STX in the mining treasuries (up to 50M STX each) ;; MAINNET: TODO ;; MAINNET: TODO - (try! (contract-call? .ccd002-treasury-mia-mining delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) - (try! (contract-call? .ccd002-treasury-nyc-mining delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + (try! (contract-call? .ccd002-treasury-mia-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) ;; add treasuries to ccd005-city-data - (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining "mining-v2")) - (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining-v2 "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining-v2 "mining-v2")) ;; disable original mining contract - (try! (contract-call? (contract-call? .ccd006-citycoin-mining set-mining-enabled false))) + (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) (ok true) ) From 34eed165ec4673234ec1efa69f3e4a79f23e857d Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 15 May 2023 17:48:26 -0700 Subject: [PATCH 10/47] chore: bump clarinet version in tests --- utils/deps.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/deps.ts b/utils/deps.ts index 9e560e16..399450fb 100644 --- a/utils/deps.ts +++ b/utils/deps.ts @@ -1,5 +1,5 @@ -export type { Account, Block, ReadOnlyFn, TxReceipt } from "https://deno.land/x/clarinet@v1.4.0/index.ts"; +export type { Account, Block, ReadOnlyFn, TxReceipt } from "https://deno.land/x/clarinet@v1.6.0/index.ts"; -export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.4.0/index.ts"; +export { Clarinet, Chain, Tx, types } from "https://deno.land/x/clarinet@v1.6.0/index.ts"; -export { assertEquals, assert } from "https://deno.land/std@0.173.0/testing/asserts.ts"; +export { assertEquals, assert } from "https://deno.land/std@0.187.0/testing/asserts.ts"; From f53389e842f40b979a5bd16f9bb8bfe302682a38 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 18 May 2023 16:46:15 -0700 Subject: [PATCH 11/47] fix: add voting fn similar to ccip-011 Also renames error constants using proposal number. --- contracts/proposals/ccip014-pox-3.clar | 84 +++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 24999993..06337914 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -1,6 +1,45 @@ +;; TRAITS + (impl-trait .proposal-trait.proposal-trait) -(define-constant ERR_PANIC (err u500)) +;; ERRORS + +(define-constant ERR_PANIC (err u1400)) +(define-constant ERR_VOTED_ALREADY (err u1401)) +(define-constant ERR_NOTHING_STACKED (err u1402)) + +;; CONSTANTS + +(define-constant CCIP_014 { + name: "", + link: "", + hash: "", +}) + +;; DATA VARS + +;; vote block heights +(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, + amount: uint + } +) + +;; PUBLIC FUNCTIONS (define-public (execute (sender principal)) (let @@ -45,3 +84,46 @@ (ok true) ) ) + +;; READ ONLY FUNCTIONS + +(define-read-only (get-proposal-info) + (some CCIP_014) +) + +(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) +) + +;; PRIVATE FUNCTIONS + +;; TODO: getter for user ID from ccd003 + +;; get block hash by height +(define-private (get-block-hash (blockHeight uint)) + (get-block-info? id-header-hash blockHeight) +) \ No newline at end of file From 2322ab543a2cd39afa5820ecc0e343f47720e79d Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 18 May 2023 16:56:58 -0700 Subject: [PATCH 12/47] fix: add voting block heights --- contracts/proposals/ccip014-pox-3.clar | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 06337914..f87a7644 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -48,6 +48,8 @@ (nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) (miaBalance (contract-call? .ccd002-treasury-mia-mining get-balance-stx)) (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) + (voteStart block-height) ;; vote starts right away + (voteEnd (+ voteStart u1000)) ;; targeting May 25, may shift ) ;; enable mining v2 treasuries in the DAO @@ -87,6 +89,8 @@ ;; READ ONLY FUNCTIONS +;; TODO: is-executable: checked by `execute` to verify vote complete + (define-read-only (get-proposal-info) (some CCIP_014) ) @@ -126,4 +130,4 @@ ;; get block hash by height (define-private (get-block-hash (blockHeight uint)) (get-block-info? id-header-hash blockHeight) -) \ No newline at end of file +) From 62d8b66197efd76b8e208f316b8f82aaa3546b70 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 19 May 2023 06:17:42 -0700 Subject: [PATCH 13/47] chore: incorporate upstream changes --- contracts/proposals/ccip014-pox-3.clar | 88 +++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 24999993..f87a7644 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -1,6 +1,45 @@ +;; TRAITS + (impl-trait .proposal-trait.proposal-trait) -(define-constant ERR_PANIC (err u500)) +;; ERRORS + +(define-constant ERR_PANIC (err u1400)) +(define-constant ERR_VOTED_ALREADY (err u1401)) +(define-constant ERR_NOTHING_STACKED (err u1402)) + +;; CONSTANTS + +(define-constant CCIP_014 { + name: "", + link: "", + hash: "", +}) + +;; DATA VARS + +;; vote block heights +(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, + amount: uint + } +) + +;; PUBLIC FUNCTIONS (define-public (execute (sender principal)) (let @@ -9,6 +48,8 @@ (nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) (miaBalance (contract-call? .ccd002-treasury-mia-mining get-balance-stx)) (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) + (voteStart block-height) ;; vote starts right away + (voteEnd (+ voteStart u1000)) ;; targeting May 25, may shift ) ;; enable mining v2 treasuries in the DAO @@ -45,3 +86,48 @@ (ok true) ) ) + +;; READ ONLY FUNCTIONS + +;; TODO: is-executable: checked by `execute` to verify vote complete + +(define-read-only (get-proposal-info) + (some CCIP_014) +) + +(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) +) + +;; PRIVATE FUNCTIONS + +;; TODO: getter for user ID from ccd003 + +;; get block hash by height +(define-private (get-block-hash (blockHeight uint)) + (get-block-info? id-header-hash blockHeight) +) From 479fbff2bfca1b1bab8ce1a944e5878f2a33e394 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 19 May 2023 06:25:31 -0700 Subject: [PATCH 14/47] fix: make clarinet check pass --- contracts/proposals/ccip014-pox-3.clar | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index f87a7644..5fdd487b 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -48,8 +48,6 @@ (nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) (miaBalance (contract-call? .ccd002-treasury-mia-mining get-balance-stx)) (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) - (voteStart block-height) ;; vote starts right away - (voteEnd (+ voteStart u1000)) ;; targeting May 25, may shift ) ;; enable mining v2 treasuries in the DAO @@ -97,8 +95,8 @@ (define-read-only (get-vote-period) (if (and - ((var-get voteStart) > u0) - ((var-get voteEnd) > u0)) + (> (var-get voteStart) u0) + (> (var-get voteEnd) u0)) ;; if both are set, return values (some { startBlock: (var-get voteStart), From 204b259f416f0adfeff9d295c84b8a97e0e1c42c Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 19 May 2023 06:58:55 -0700 Subject: [PATCH 15/47] fix: update mining tests for claims These two were failing after upgrading Clarinet, but seems to be a regression as the structure of this function didn't change. ERR_ALREADY_CLAIMED short-circuits before ERR_MINER_NOT_WINNER in these scenarios. --- tests/extensions/ccd006-citycoin-mining.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/extensions/ccd006-citycoin-mining.test.ts b/tests/extensions/ccd006-citycoin-mining.test.ts index 412da868..213c37de 100644 --- a/tests/extensions/ccd006-citycoin-mining.test.ts +++ b/tests/extensions/ccd006-citycoin-mining.test.ts @@ -1143,14 +1143,14 @@ Clarinet.test({ let winner: number; - if (miningClaimUser2.receipts[0].result === "(err u6010)") { + if (miningClaimUser2.receipts[0].result === "(err u6014)") { //console.log("USER 1 WINS"); miningClaimUser1.receipts[0].result.expectOk().expectBool(true); - miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); winner = 1; } else { //console.log("USER 2 WINS"); - miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); miningClaimUser2.receipts[0].result.expectOk().expectBool(true); winner = 2; } @@ -1437,14 +1437,14 @@ Clarinet.test({ let winner: number; - if (miningClaimUser2.receipts[0].result === "(err u6010)") { + if (miningClaimUser2.receipts[0].result === "(err u6014)") { //console.log("USER 1 WINS"); miningClaimUser1.receipts[0].result.expectOk().expectBool(true); - miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); winner = 1; } else { //console.log("USER 2 WINS"); - miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); miningClaimUser2.receipts[0].result.expectOk().expectBool(true); winner = 2; } From f826fcb8773ddfcdddda13dc7672efcbb6d4bb96 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 05:35:44 -0700 Subject: [PATCH 16/47] fix: bump epoch in Clarinet.toml to 2.4 --- Clarinet.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Clarinet.toml b/Clarinet.toml index 41f0da8e..1bf7aeaf 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -146,7 +146,7 @@ path = "contracts/proposals/ccip013-activation.clar" [contracts.ccip014-pox-3] path = "contracts/proposals/ccip014-pox-3.clar" clarity_version = 2 -epoch = 2.1 +epoch = 2.4 # CITYCOINS PROTOCOL TRAITS @@ -188,7 +188,7 @@ path = "tests/contracts/external/test-ccext-nft-nyc.clar" [contracts.mock-pox-3] path = "tests/contracts/external/mock-pox-3.clar" clarity_version = 2 -epoch = 2.1 +epoch = 2.4 # CITYCOINS LEGACY CONTRACTS From 05687001e03a454c5ba54d2ee6abd235ad20a6d3 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 08:52:49 -0700 Subject: [PATCH 17/47] fix: add mia and nyc voting calculations --- contracts/proposals/ccip014-pox-3.clar | 151 ++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 5fdd487b..1cc7b707 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -7,6 +7,9 @@ (define-constant ERR_PANIC (err u1400)) (define-constant ERR_VOTED_ALREADY (err u1401)) (define-constant ERR_NOTHING_STACKED (err u1402)) +(define-constant ERR_USER_NOT_FOUND (err u1403)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u1404)) +(define-constant ERR_NO_CITY_ID (err u1405)) ;; CONSTANTS @@ -16,6 +19,12 @@ hash: "", }) +(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 u876) ;; 0.876 or 87.6% +;; MIA votes scaled to make 1 MIA = 1 NYC +;; full calculation available in CCIP-014 + ;; DATA VARS ;; vote block heights @@ -35,7 +44,9 @@ uint ;; user ID { ;; vote vote: bool, - amount: uint + mia: uint, + nyc: uint, + total: uint, } ) @@ -85,6 +96,79 @@ ) ) +(define-public (vote-on-ccip014 (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! (and + (>= block-height (var-get voteStart)) + (<= block-height (var-get voteEnd))) + 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 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))) + ) + (begin + (var-set noVotes (+ (var-get noVotes) u1)) + (var-set noTotal (+ (var-get noTotal) (get total record))) + ) + ) + ) + ;; if the voterRecord does not exist + (let + ( + ;; TODO + (scaledVoteMia u0) + (scaledVoteNyc u0) + (voteMia u0) + (voteNyc u0) + (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 ;; TODO: is-executable: checked by `execute` to verify vote complete @@ -121,11 +205,72 @@ (map-get? UserVotes id) ) -;; PRIVATE FUNCTIONS +;; 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 54 / first block 74,897 + ;; cycle 2 / u4500 used in tests + (cycle54Hash (unwrap! (get-block-hash u4500) none)) + (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle54Amount (get stacked cycle54Data)) + ;; MAINNET: MIA cycle 55 / first block 76,997 + ;; cycle 3 / u6600 used in tests + (cycle55Hash (unwrap! (get-block-hash u6600) none)) + (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle55Amount (get stacked cycle55Data)) + ;; MIA vote calculation + (avgStacked (/ (+ (scale-up cycle54Amount) (scale-up cycle55Amount)) u2)) + (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle54Amount u0) (> cycle55Amount 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 54 / first block 75,249, cycle 2 / u4500 used in tests + ;; mainnet: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2 + (cycle54Hash (unwrap! (get-block-hash u4500) none)) + (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle54Amount (get stacked cycle54Data)) + ;; NYC cycle 55 / first block 77,349, cycle 3 / u6600 used in tests + ;; mainnet: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2 + (cycle55Hash (unwrap! (get-block-hash u6600) none)) + (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle55Amount (get stacked cycle55Data)) + ;; NYC vote calculation + (scaledVote (/ (+ (scale-up cycle54Amount) (scale-up cycle55Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle54Amount u0) (> cycle55Amount u0)) none) + ;; return scaled or unscaled value + (if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR))) + ) +) -;; TODO: getter for user ID from ccd003 +;; 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) +) From 81067f96841c71a537f1b97d537722eb3651fac0 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 09:29:41 -0700 Subject: [PATCH 18/47] fix: correct block heights and add BTC/STX equiv --- contracts/proposals/ccip014-pox-3.clar | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 1cc7b707..b2041c15 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -115,7 +115,7 @@ (begin ;; check vote is not the same as before (asserts! (not (is-eq (get vote record) vote)) ERR_VOTED_ALREADY) - ;; record the vote for the user + ;; record the new vote for the user (map-set UserVotes voterId (merge record { vote: vote }) ) @@ -124,8 +124,12 @@ (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))) ) @@ -135,10 +139,10 @@ (let ( ;; TODO - (scaledVoteMia u0) - (scaledVoteNyc u0) - (voteMia u0) - (voteNyc u0) + (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 @@ -211,12 +215,12 @@ (define-read-only (get-mia-vote (cityId uint) (userId uint) (scaled bool)) (let ( - ;; MAINNET: MIA cycle 54 / first block 74,897 + ;; MAINNET: MIA cycle 54 / first block BTC 779,450 STX 97,453 ;; cycle 2 / u4500 used in tests (cycle54Hash (unwrap! (get-block-hash u4500) none)) - (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) (cycle54Amount (get stacked cycle54Data)) - ;; MAINNET: MIA cycle 55 / first block 76,997 + ;; MAINNET: MIA cycle 55 / first block BTC 781,550 STX 99,112 ;; cycle 3 / u6600 used in tests (cycle55Hash (unwrap! (get-block-hash u6600) none)) (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) @@ -238,13 +242,13 @@ (define-read-only (get-nyc-vote (cityId uint) (userId uint) (scaled bool)) (let ( - ;; NYC cycle 54 / first block 75,249, cycle 2 / u4500 used in tests - ;; mainnet: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2 + ;; NYC cycle 54 / first block BTC 779,450 STX 97,453 + ;; cycle 2 / u4500 used in tests (cycle54Hash (unwrap! (get-block-hash u4500) none)) (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) (cycle54Amount (get stacked cycle54Data)) - ;; NYC cycle 55 / first block 77,349, cycle 3 / u6600 used in tests - ;; mainnet: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-core-v2 + ;; NYC cycle 55 / first block BTC 781,550 STX 99,112 + ;; cycle 3 / u6600 used in tests (cycle55Hash (unwrap! (get-block-hash u6600) none)) (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) (cycle55Amount (get stacked cycle55Data)) From a7d7f60e57c875b9904af97ac57dffde929ca9ac Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 10:55:49 -0700 Subject: [PATCH 19/47] fix: add check that vote passed to execute --- contracts/proposals/ccip014-pox-3.clar | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index b2041c15..095641ae 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -9,7 +9,9 @@ (define-constant ERR_NOTHING_STACKED (err u1402)) (define-constant ERR_USER_NOT_FOUND (err u1403)) (define-constant ERR_PROPOSAL_NOT_ACTIVE (err u1404)) -(define-constant ERR_NO_CITY_ID (err u1405)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u1405)) +(define-constant ERR_NO_CITY_ID (err u1406)) +(define-constant ERR_VOTE_FAILED (err u1407)) ;; CONSTANTS @@ -61,6 +63,9 @@ (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) ) + ;; check vote details + (try! (is-executable)) + ;; enable mining v2 treasuries in the DAO (try! (contract-call? .base-dao set-extensions (list @@ -175,7 +180,14 @@ ;; READ ONLY FUNCTIONS -;; TODO: is-executable: checked by `execute` to verify vote complete +(define-read-only (is-executable) + (begin + (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) + (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) + (asserts! (> (var-get yesTotal) (var-get noTotal)) ERR_VOTE_FAILED) + (ok true) + ) +) (define-read-only (get-proposal-info) (some CCIP_014) From b121315c7b2d0ad812b784a58666beea90f52776 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 12:12:48 -0700 Subject: [PATCH 20/47] fix: clone mining models and test for v2 --- .../ccd006-citycoin-mining-v2.model.ts | 109 ++ .../ccd006-citycoin-mining-v2.test.ts | 1661 +++++++++++++++++ 2 files changed, 1770 insertions(+) create mode 100644 models/extensions/ccd006-citycoin-mining-v2.model.ts create mode 100644 tests/extensions/ccd006-citycoin-mining-v2.test.ts diff --git a/models/extensions/ccd006-citycoin-mining-v2.model.ts b/models/extensions/ccd006-citycoin-mining-v2.model.ts new file mode 100644 index 00000000..a2049024 --- /dev/null +++ b/models/extensions/ccd006-citycoin-mining-v2.model.ts @@ -0,0 +1,109 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +export enum ErrCode { + ERR_UNAUTHORIZED = 6000, + ERR_INVALID_CITY, + ERR_NO_ACTIVATION_DETAILS, + ERR_INACTIVE_CITY, + ERR_INVALID_USER, + ERR_INVALID_TREASURY, + ERR_INVALID_DELAY, + ERR_INVALID_COMMITS, + ERR_NOT_ENOUGH_FUNDS, + ERR_ALREADY_MINED, + ERR_REWARD_IMMATURE, + ERR_NO_VRF_SEED, + ERR_DID_NOT_MINE, + ERR_NO_MINER_DATA, + ERR_ALREADY_CLAIMED, + ERR_MINER_NOT_WINNER, + ERR_MINING_DISABLED, +} + +export class CCD006CityMiningV2 { + name: string; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account, name: string) { + this.name = name; + this.chain = chain; + this.deployer = deployer; + } + + // Authorization + + isDaoOrExtension(): ReadOnlyFn { + return this.callReadOnlyFn("is-dao-or-extension"); + } + + callback(sender: Account, memo: string) { + return Tx.contractCall(this.name, "callback", [types.principal(sender.address), types.buff(memo)], sender.address); + } + + // Internal DAO functions + + setRewardDelay(sender: Account, delay: number) { + return Tx.contractCall(this.name, "set-reward-delay", [types.uint(delay)], sender.address); + } + + setMiningEnabled(sender: Account, status: boolean) { + return Tx.contractCall(this.name, "set-mining-enabled", [types.bool(status)], sender.address); + } + + // Public functions + + mine(sender: Account, cityName: string, amounts: Array<number>) { + return Tx.contractCall(this.name, "mine", [types.ascii(cityName), types.list(amounts.map((entry) => types.uint(entry)))], sender.address); + } + + claimMiningReward(sender: Account, cityName: string, claimHeight: number) { + return Tx.contractCall(this.name, "claim-mining-reward", [types.ascii(cityName), types.uint(claimHeight)], sender.address); + } + + // Read only functions + + getRewardDelay(): ReadOnlyFn { + return this.callReadOnlyFn("get-reward-delay", []); + } + + getMiningStats(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-mining-stats", [types.uint(cityId), types.uint(blockHeight)]); + } + + hasMinedAtBlock(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { + return this.callReadOnlyFn("has-mined-at-block", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); + } + + getMiner(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { + return this.callReadOnlyFn("get-miner", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); + } + + getHighValue(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-high-value", [types.uint(cityId), types.uint(blockHeight)]); + } + + getBlockWinner(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-block-winner", [types.uint(cityId), types.uint(blockHeight)]); + } + + isBlockWinner(cityId: number, user: string, claimHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("is-block-winner", [types.uint(cityId), types.principal(user), types.uint(claimHeight)]); + } + + getCoinbaseAmount(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-coinbase-amount", [types.uint(cityId), types.uint(blockHeight)]); + } + + isMiningEnabled(): ReadOnlyFn { + return this.callReadOnlyFn("is-mining-enabled", []); + } + + // helper for calling read only functions + + private callReadOnlyFn(method: string, args: Array<any> = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts new file mode 100644 index 00000000..0bc20117 --- /dev/null +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -0,0 +1,1661 @@ +/** + * Test class is structured; + * 0. AUTHORIZATION CHECKS + * 1. mine + * 2. claim-mining-reward + * 3. reward-delay + * 4. mining status + * 5. read-only functions + */ +import { Account, assert, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { constructAndPassProposal, EXTENSIONS, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { CCD002Treasury } from "../../models/extensions/ccd002-treasury.model.ts"; +import { CCD003UserRegistry } from "../../models/extensions/ccd003-user-registry.model.ts"; +import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.ts"; +import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; +import { CCD006CityMiningV2 } from "../../models/extensions/ccd006-citycoin-mining-v2.model.ts"; +import { CCD010CoreV2Adapter } from "../../models/extensions/ccd010-core-v2-adapter.model.ts"; +import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; + +// ============================= +// INTERNAL DATA / FUNCTIONS +// ============================= + +const rewardDelay = 100; + +// reusable city data + +type CityData = { + cityId: number; + cityName: string; + treasuryV1Contract: string; + treasuryV1Id: number; + treasuryV1Name: string; + treasuryV2Contract: string; + treasuryV2Id: number; + treasuryV2Name: string; +}; + +const mia: CityData = { + cityId: 1, + cityName: "mia", + treasuryV1Contract: "ccd002-treasury-mia-mining", + treasuryV1Id: 1, + treasuryV1Name: "mining", + treasuryV2Contract: "ccd002-treasury-mia-mining-v2", + treasuryV2Id: 2, + treasuryV2Name: "mining-v2", +}; + +const nyc: CityData = { + cityId: 2, + cityName: "nyc", + treasuryV1Contract: "ccd002-treasury-nyc-mining", + treasuryV1Id: 1, + treasuryV1Name: "mining", + treasuryV2Contract: "ccd002-treasury-nyc-mining-v2", + treasuryV2Id: 2, + treasuryV2Name: "mining-v2", +}; + +// reusable mining functions + +const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, userId: number, miningStatsAt: any, minerAt: any) => { + let expectedStats: any = { + amount: types.uint(miningStatsAt.amount), + claimed: types.bool(miningStatsAt.claimed), + miners: types.uint(miningStatsAt.miners), + }; + assertEquals(ccd006CityMining.getMiningStatsAtBlock(cityId, height).result.expectTuple(), expectedStats); + + expectedStats = { + commit: types.uint(minerAt.commit), + high: types.uint(minerAt.high), + low: types.uint(minerAt.low), + winner: types.bool(minerAt.winner), + }; + assertEquals(ccd006CityMining.getMinerAtBlock(cityId, height, userId).result.expectTuple(), expectedStats); +}; + +const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006CityMining, chain: Chain, sender: Account): any => { + const entries: number[] = [10]; + const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); + const claimHeight = miningBlock.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + const miningClaimBlock = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight), ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); + + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); + miningBlock.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); + let winner = 0; + let coinbase = 0; + if (miningClaimBlock.receipts[0].result === "(ok true)") { + //console.log("======== USER 1 WINS =========================") + ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(1); + coinbase = Number(ccd006CityMining.getCoinbaseAmount(mia.cityId, claimHeight).result.substring(1)); + miningClaimBlock.receipts[0].result.expectOk().expectBool(true); + /** + console.log("getCoinbaseAmount : " + coinbase) + console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(mia.cityId, user1.address, claimHeight).result.expectSome().expectTuple()) + console.log("getMiningStatsAtBlock : ", ccd006CityMining.getMiningStatsAtBlock(mia.cityId, claimHeight)) + */ + winner = 1; + } else if (miningClaimBlock.receipts[1].result === "(ok true)") { + //console.log("======== USER 2 WINS =========================") + ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(2); + //miningClaimBlock.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimBlock.receipts[1].result.expectOk().expectBool(true); + coinbase = Number(ccd006CityMining.getCoinbaseAmount(mia.cityId, claimHeight).result.substring(1)); + winner = 2; + /** + console.log("getCoinbaseAmount : " + coinbase) + console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(mia.cityId, user2.address, claimHeight).result.expectSome().expectTuple()) + console.log("getMiningStatsAtBlock : ", ccd006CityMining.getMiningStatsAtBlock(mia.cityId, claimHeight)) + */ + } else { + console.log("======== NOONE WINS ========================="); + return 3; + } + return { miningBlock, miningClaimBlock, claimHeight, winner, coinbase }; +}; + +// ============================= +// 0. AUTHORIZATION CHECKS +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: is-dao-or-extension() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + + // assert + ccd006CityMining.isDaoOrExtension().result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +// Extension callback + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: callback() succeeds when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const { receipts } = chain.mineBlock([ccd006CityMining.callback(sender, "test")]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectOk().expectBool(true); + }, +}); + +// ============================= +// 1. mine +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city is not registered", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [10, 10]; + const { receipts } = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city is not active", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [10, 10]; + // create city ids + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set city details (fails before this check) + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city treasury (fails before this check) + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_012); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INACTIVE_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city details are not set", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [10, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + //passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_ACTIVATION_DETAILS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city treasury is not set", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [10, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if mining contract is not a valid dao extension", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [10, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_001); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); + ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD003UserRegistry.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if user has insufficient balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [100000000000001]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NOT_ENOUGH_FUNDS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() succeeds if user's cumulative commit uses their exact balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [50000000000000, 50000000000000]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + ccd002Treasury.getBalanceStx().result.expectUint(100000000000000); + block.receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if user's cumulative commit leaves insufficient balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [50000000000000, 50000000000000, 1]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + ccd002Treasury.getBalanceStx().result.expectUint(0); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NOT_ENOUGH_FUNDS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city is inactive", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries = [10, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_003); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); + ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INACTIVE_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if called with no commit amounts", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = []; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); + ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_COMMITS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if a commit amount in the list is zero", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10, 10, 10, 0, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_COMMITS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() succeeds and mines 1 block for 1 user", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); + + // assert + const firstBlock = block.height - 1; + const lastBlock = firstBlock; + const totalAmount = 10; + const totalBlocks = 1; + const userId = 1; + + block.receipts[0].result.expectOk().expectBool(true); + + block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${mia.treasuryV1Contract}`); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + //console.log(block.receipts[0].events[1].contract_event.value) + //ccd006CityMining.isBlockWinner(mia.cityId, user.address, firstBlock).result.expectBool(true) + const expectedStats2 = { + commit: types.uint(10), + high: types.uint(10), + low: types.uint(0), + winner: types.bool(false), + }; + assertEquals(ccd006CityMining.getMinerAtBlock(mia.cityId, firstBlock, userId).result.expectTuple(), expectedStats2); + const expectedStats = { + amount: types.uint(10), + claimed: types.bool(false), + miners: types.uint(1), + }; + assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedStats); + ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); + + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + block.receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() succeeds and mines 200 blocks for 1 user", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const userId = 1; + const commitAmount = 10; + const numberOfBlocks = 200; + const entries = new Array<number>(numberOfBlocks).fill(commitAmount); + const totalAmount = entries.reduce((a, b) => a + b, 0); + const totalBlocks = entries.length; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); + + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[0].events.expectSTXTransferEvent(totalAmount, user.address, `${sender.address}.${mia.treasuryV1Contract}`); + + // mining stats at block + const expectedStats = { + amount: types.uint(commitAmount), + claimed: types.bool(false), + miners: types.uint(1), + }; + // miner stats at each block + const expectedMinerStats = { + commit: types.uint(commitAmount), + high: types.uint(commitAmount), + low: types.uint(0), + winner: types.bool(false), + }; + for (let i = 0; i < entries.length; i++) { + assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMinerAtBlock(mia.cityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); + } + + ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + block.receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() successfully mines 100 blocks for 3 users", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!]; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const userIds = [1, 2, 3]; + const commitAmount = 100; + const numberOfBlocks = 100; + const entries = new Array<number>(numberOfBlocks).fill(commitAmount); + const totalCommit = entries.reduce((a, b) => a + b, 0); + const totalBlocks = entries.length; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + const block = chain.mineBlock([ccd006CityMining.mine(users[0], mia.cityName, entries), ccd006CityMining.mine(users[1], mia.cityName, entries), ccd006CityMining.mine(users[2], mia.cityName, entries)]); + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + + for (let i = 0; i < userIds.length; i++) { + // check that each event succeeded + block.receipts[i].result.expectOk().expectBool(true); + // check that each event transferred the correct amount to the correct address + block.receipts[i].events.expectSTXTransferEvent(totalCommit, users[i].address, `${sender.address}.${mia.treasuryV1Contract}`); + } + + // mining stats at block + const expectedStats = { + amount: types.uint(commitAmount * users.length), + claimed: types.bool(false), + miners: types.uint(users.length), + }; + // loop through each block to check miner stats + for (let i = 0; i < entries.length; i++) { + assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedStats); + // loop through each user + for (let j = 0; j < userIds.length; j++) { + // check the data + const lastCommit = commitAmount * j; + const currentCommit = commitAmount * (j + 1); + const expectedMinerStats = { + commit: types.uint(commitAmount), + high: types.uint(currentCommit), + low: types.uint(j === 0 ? 0 : lastCommit + 1), + winner: types.bool(false), + }; + assertEquals(ccd006CityMining.getMinerAtBlock(mia.cityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); + } + } + + // check the print message for each user + for (let i = 0; i < userIds.length; i++) { + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalCommit)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userIds[i])}}`; + block.receipts[i].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + block.receipts[i].result.expectOk().expectBool(true); + } + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if user has already mined", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const totalAmount = 30; + const totalBlocks = 3; + const userId = 1; + const entries: number[] = [10, 10, 10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries), ccd006CityMining.mine(sender, mia.cityName, entries)]); + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_MINED); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + block.receipts[0].events.expectSTXTransferEvent(totalAmount, sender.address, `${sender.address}.${mia.treasuryV1Contract}`); + + // block.receipts[1].events.expectSTXTransferEvent(totalAmount, sender.address, `${sender.address}.${mia.treasuryV1Contract}`); + + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + // Expecting ERR_ALREADY_MINED if same user tried mine twice at same height ? + block.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_MINED); + // and for the treasury balance to show the first tx only .. + ccd002Treasury.getBalanceStx().result.expectUint(totalAmount); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() keeps track of mining stats for 4 users mining 3 blocks concurrently", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!, accounts.get("wallet_4")!]; + + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd003UserRegistry = new CCD003UserRegistry(chain, sender, "ccd003-user-registry"); + + const totalAmount = 120; + const totalBlocks = 3; + const entries: number[] = [10, 10, 10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + const block = chain.mineBlock([ccd006CityMining.mine(users[0], mia.cityName, entries), ccd006CityMining.mine(users[1], mia.cityName, entries), ccd006CityMining.mine(users[2], mia.cityName, entries), ccd006CityMining.mine(users[3], mia.cityName, entries)]); + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + let miningStatsAt, minerAt; + for (let idx = 0; idx < 4; idx++) { + block.receipts[idx].result.expectOk().expectBool(true); + ccd003UserRegistry + .getUserId(users[idx].address) + .result.expectSome() + .expectUint(idx + 1); + ccd003UserRegistry + .getUser(idx + 1) + .result.expectSome() + .expectPrincipal(users[idx].address); + block.receipts[idx].events.expectSTXTransferEvent(30, users[idx].address, `${sender.address}.${mia.treasuryV1Contract}`); + + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount / 4)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(idx + 1)}}`; + block.receipts[idx].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + } + + for (let idx1 = 0; idx1 < entries.length; idx1++) { + // 3 blocks + for (let idx = 0; idx < users.length; idx++) { + // 4 users + miningStatsAt = { amount: users.length * entries[idx1], claimed: false, miners: users.length }; + minerAt = { commit: 10, high: 10 * (idx + 1), low: idx === 0 ? 0 : idx * 10 + 1, winner: false }; + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock + idx1), (idx + 1), miningStatsAt, minerAt); + checkMiningData(ccd006CityMining, mia.cityId, firstBlock + idx1, idx + 1, miningStatsAt, minerAt); + } + } + + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + // check that total balance was transferred to treasury + ccd002Treasury.getBalanceStx().result.expectUint(totalAmount); + ccd006CityMining.getHighValue(mia.cityId, firstBlock).result.expectUint(totalAmount / 3); + ccd006CityMining.getHighValue(mia.cityId, lastBlock).result.expectUint(totalAmount / 3); + ccd006CityMining.getHighValue(mia.cityId, lastBlock + 1).result.expectUint(0); + }, +}); + +// ============================= +// 2. claim-mining-reward +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: is-block-winner() correctly identifies winning miner who has not claimed", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + const totalAmount = 10; + const totalBlocks = 1; + const entries: number[] = [10]; + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); + // console.log(`miningBlock:\n${JSON.stringify(miningBlock, null, 2)}}`); + const claimHeight = miningBlock.height - 1; + const lastBlock = claimHeight + totalBlocks - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + // assert + miningBlock.receipts[0].result.expectOk().expectBool(true); + + // Check stx transfer events + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); + + // Check mining events + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); + const expected = { + claimed: types.bool(false), + winner: types.bool(true), + }; + const isBlockWinner = ccd006CityMining.isBlockWinner(mia.cityId, user1.address, claimHeight); + //console.log(JSON.stringify(isBlockWinner, null, 2)); + // console.log(`isBlockWinner:\n${JSON.stringify(isBlockWinner, null, 2)}}`); + assertEquals(isBlockWinner.result.expectSome().expectTuple(), expected); + // is-block-winner calculates the winning status of given user. + // get-block-winner reads it from the map which is written by claim-mining-reward. + // so user1 is not returned by the following. This is correct, since the map isn't written + // to until the reward is claimed, + // we would expect the value to be none here while the output of isBlockWinner() above + // will be (some (claimed false) (winner true)). + ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectNone(); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: is-block-winner() correctly identifies winning miner who has claimed", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + const totalAmount = 10; + const totalBlocks = 1; + const entries: number[] = [10]; + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); + const miningHeight = miningBlock.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + const miningClaimBlock = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, miningHeight)]); + + // assert + miningBlock.receipts[0].result.expectOk().expectBool(true); + // Check mining event + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(miningHeight)}, lastBlock: ${types.uint(miningHeight)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + // Check mining claim event + expectedPrintMsg = `{cityId: u1, cityName: "mia", claimHeight: ${types.uint(miningHeight)}, event: "mining-claim", userId: ${types.uint(1)}}`; + miningClaimBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + // Check stx transfer events + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); + // check mia token balances + gt.getBalance(user1.address).result.expectOk().expectUint(10); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); + const expected = { + claimed: types.bool(true), + winner: types.bool(true), + }; + assertEquals(ccd006CityMining.isBlockWinner(mia.cityId, user1.address, miningHeight).result.expectSome().expectTuple(), expected); + ccd006CityMining.getBlockWinner(mia.cityId, miningHeight).result.expectSome().expectUint(1); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible for an unknown city", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const { receipts } = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, 50)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(100); + receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if current tip height is less than maturity height ", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + const block = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, 50)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(100); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_REWARD_IMMATURE); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if current tip height is equal to maturity height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + const claimBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + chain.mineEmptyBlock(rewardDelay - 1); + const block = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, claimBlock.height)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_REWARD_IMMATURE); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if user is not registered", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + const claimHeight = 5; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + const block = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, claimHeight)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_USER); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() returns ERR_NO_MINER_DATA if user did not mine in that block", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + const claimHeight = 6; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + const block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if a user tries claiming another users rewards", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + let block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); + const claimHeight = block.height - 1; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_USER); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if there is nothing to mint at the given claim height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); + const claimHeight = block.height - 1; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails with ERR_NOTHING_TO_MINT if the coinbase amounts have not been set for the given city", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[0].result.expectOk().expectBool(true); + const firstBlock = block.height - 1; + const lastBlock = firstBlock; + const totalAmount = 10; + const totalBlocks = 1; + const userId = 1; + block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${mia.treasuryV1Contract}`); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; + //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; + // dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); + + const claimHeight = block.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); + + // assert + + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails with ERR_NOTHING_TO_MINT if called at the wrong claim height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[0].result.expectOk().expectBool(true); + const firstBlock = block.height - 1; + const lastBlock = firstBlock; + const totalAmount = 10; + const totalBlocks = 1; + const userId = 1; + block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${mia.treasuryV1Contract}`); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; + //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); + + const claimHeight = block.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); + + // assert + + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if user is not the winner or if there is nothing to mint", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const totalAmount = 10; + const totalBlocks = 1; + const entries: number[] = [10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_007); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + const block1 = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); + const firstBlock = block1.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + chain.mineEmptyBlock(rewardDelay); + const block2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, firstBlock), ccd006CityMining.claimMiningReward(user2, mia.cityName, firstBlock)]); + + // assert + + block1.receipts[0].result.expectOk().expectBool(true); + block1.receipts[1].result.expectOk().expectBool(true); + + if (block2.receipts[0].result === "(err u6011)") { + //console.log("USER 2 WINS"); + block2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + block2.receipts[1].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + } else { + //console.log("USER 1 WINS"); + block2.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + block2.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + } + + block1.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); + block1.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); + + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; + block1.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + block1.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + + //ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectUint(2); + ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() user makes successful claim", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const totalAmount = 10; + const totalBlocks = 1; + const entries = [10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); + // console.log(JSON.stringify(miningBlock, null, 2)); + const claimHeight = miningBlock.height - 1; + const lastBlock = claimHeight + totalBlocks - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + const miningClaimUser1 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); + const miningClaimUser2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); + + // assert + + const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(mia.cityId).result.expectTuple(); + // verify coinbase amounts + const expectedAmounts = { + cbaBonus: types.uint(10), + cba1: types.uint(100), + cba2: types.uint(1000), + cba3: types.uint(10000), + cba4: types.uint(100000), + cba5: types.uint(1000000), + cbaDefault: types.uint(10000000), + }; + assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); + // verify coinbase thresholds + const expectedThresholds = { + cbt1: types.uint(50), + cbt2: types.uint(60), + cbt3: types.uint(70), + cbt4: types.uint(80), + cbt5: types.uint(90), + }; + assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); + + miningBlock.receipts[0].result.expectOk().expectBool(true); + miningBlock.receipts[0].events.expectSTXTransferEvent(totalAmount, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); + miningBlock.receipts[1].result.expectOk().expectBool(true); + miningBlock.receipts[1].events.expectSTXTransferEvent(totalAmount, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); + + //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; + //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (2), miningStatsAt, minerAt); + + //console.log(`miningClaimUser1:\n${JSON.stringify(miningClaimUser1, null, 2)}\n`); + //console.log(`miningClaimUser2:\n${JSON.stringify(miningClaimUser2, null, 2)}\n`); + + let winner: number; + + if (miningClaimUser2.receipts[0].result === "(err u6014)") { + //console.log("USER 1 WINS"); + miningClaimUser1.receipts[0].result.expectOk().expectBool(true); + miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + winner = 1; + } else { + //console.log("USER 2 WINS"); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + miningClaimUser2.receipts[0].result.expectOk().expectBool(true); + winner = 2; + } + + // {cityId: u1, cityName: \"mia\", cityTreasury: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining, event: \"mining\", firstBlock: u10, lastBlock: u10, totalAmount: u10, totalBlocks: u1, userId: u1}"} + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; + // console.log(JSON.stringify(miningBlock.receipts[0].events), null, 2); + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + miningBlock.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + + //ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectUint(2); + ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(winner); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() two miners compete and each wins within 10% of half the time", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + // add mining treasury + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + // assert + let result; + let winner1 = 0; + let winner2 = 0; + let count1 = 0; + let count2 = 0; + const runs = 100; + for (let i = 0; i < runs; i++) { + result = twoMinersMine(user1, user2, ccd006CityMining, chain, sender); + if (result.winner === 1) { + count1 = count1 + result.coinbase; + winner1++; + } else if (result.winner === 2) { + count2 = count2 + result.coinbase; + winner2++; + } + } + gt.getBalance(user1.address).result.expectOk().expectUint(count1); + gt.getBalance(user2.address).result.expectOk().expectUint(count2); + // ensure that each wins within 10% of half the time + assert(winner1 > runs / 2 - (runs * 10) / 100); + assert(winner2 > runs / 2 - (runs * 10) / 100); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if user claims at incorrect height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + // add mining treasury + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + // assert + const entries = [10]; + const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); + const claimHeight = miningBlock.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + const miningClaimBlock = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight + 1), ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight - 1), ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); + + //console.log(`miningClaimBlock: ${JSON.stringify(miningClaimBlock, null, 2)}}`); + + // assert + miningClaimBlock.receipts[2].result.expectOk().expectBool(true); + miningClaimBlock.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + miningClaimBlock.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + }, +}); + +// ============================= +// 3. REWARD DELAY +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-reward-delay() can only be called by the dao", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const { receipts } = chain.mineBlock([ccd006CityMining.setRewardDelay(sender, 50)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(100); + receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-reward-delay() cannot set a zero block delay", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_004); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(100); + receipts[3].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_DELAY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-reward-delay() successfully changes the reward delay when called by the dao", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + ccd006CityMining.getRewardDelay().result.expectUint(100); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_003); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(50); + assertEquals(receipts.length, 4); + receipts[3].result.expectOk(); + }, +}); + +// ============================= +// 4. MINING STATUS +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if mining is disabled in the contract", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); + + // assert + const firstBlock = block.height - 1; + const userId = 1; + + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + + const expectedMiningStats = { + amount: types.uint(0), + claimed: types.bool(false), + miners: types.uint(0), + }; + assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedMiningStats); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() returns ERR_NO_MINER_DATA after mining is disabled if user did not mine in that block", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + const claimHeight = 6; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + const block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); + + // assert + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() succeeds after mining is disabled", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const entries = [10]; + const totalAmount = entries.reduce((a, b) => a + b, 0); + const totalBlocks = entries.length; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); + + const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); + // console.log(JSON.stringify(miningBlock, null, 2)); + const claimHeight = miningBlock.height - 1; + const lastBlock = claimHeight + totalBlocks - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + // disable mining + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + + const miningClaimUser1 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); + const miningClaimUser2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); + + // assert + + const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(mia.cityId).result.expectTuple(); + // verify coinbase amounts + const expectedAmounts = { + cbaBonus: types.uint(10), + cba1: types.uint(100), + cba2: types.uint(1000), + cba3: types.uint(10000), + cba4: types.uint(100000), + cba5: types.uint(1000000), + cbaDefault: types.uint(10000000), + }; + assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); + // verify coinbase thresholds + const expectedThresholds = { + cbt1: types.uint(50), + cbt2: types.uint(60), + cbt3: types.uint(70), + cbt4: types.uint(80), + cbt5: types.uint(90), + }; + assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); + + miningBlock.receipts[0].result.expectOk().expectBool(true); + miningBlock.receipts[1].result.expectOk().expectBool(true); + + let winner: number; + + if (miningClaimUser2.receipts[0].result === "(err u6014)") { + //console.log("USER 1 WINS"); + miningClaimUser1.receipts[0].result.expectOk().expectBool(true); + miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + winner = 1; + } else { + //console.log("USER 2 WINS"); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + miningClaimUser2.receipts[0].result.expectOk().expectBool(true); + winner = 2; + } + + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); + miningBlock.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); + // {cityId: u1, cityName: \"mia\", cityTreasury: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining, event: \"mining\", firstBlock: u10, lastBlock: u10, totalAmount: u10, totalBlocks: u1, userId: u1}"} + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; + // console.log(JSON.stringify(miningBlock.receipts[0].events), null, 2); + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + miningBlock.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); + + //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); + ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); + + //ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectUint(2); + ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(winner); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-mining-enabled() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + // act + const block = chain.mineBlock([ccd006CityMining.setMiningEnabled(sender, true)]); + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-mining-enabled() returns true after deployment", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + // act + // assert + ccd006CityMining.isMiningEnabled().result.expectBool(true); + }, +}); + +// ============================= +// 5. READ-ONLY FUNCTIONS +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if coinbase info isn't set", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + + // act + const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 100); + + // assert + result.expectUint(0); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if the city activation details do not exist", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + // get MIA/NYC city IDs + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city status to activated + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // set city activation details + // passProposal(PROPOSALS.TEST_CCD005_CITY_DATA_004); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + + // act + const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 100); + + // assert + result.expectUint(0); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if the block height is before the city's activation height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + // get MIA/NYC city IDs + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city status to activated + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // set city activation details + // activation block = 10 + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_004); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + + // act + const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 5); + + // assert + result.expectUint(0); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns the expected amounts", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + // get MIA/NYC city IDs + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set city activation details + // activation block = 5 + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city status to activated + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + + // act + const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(mia.cityId).result.expectTuple(); + // verify coinbase amounts + const expectedAmounts = { + cbaBonus: types.uint(10), + cba1: types.uint(100), + cba2: types.uint(1000), + cba3: types.uint(10000), + cba4: types.uint(100000), + cba5: types.uint(1000000), + cbaDefault: types.uint(10000000), + }; + + // verify coinbase thresholds + const expectedThresholds = { + cbt1: types.uint(50), + cbt2: types.uint(60), + cbt3: types.uint(70), + cbt4: types.uint(80), + cbt5: types.uint(90), + }; + + // verify coinbase details + const expectedDetails = { + bonus: types.uint(20), + epoch: types.uint(1), + }; + + // assert + + // const activation = ccd005CityData.getCityActivationDetails(mia.cityId).result.expectSome().expectTuple(); + // console.log(`activation: ${JSON.stringify(activation)}`); + + // verify coinbase details + assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); + assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); + assertEquals(coinbaseInfo.details.expectSome().expectTuple(), expectedDetails); + + // get coinbase amount based on thresholds + const thresholds = [25, 50, 60, 70, 80, 90]; + let counter = 0; + for (const threshold of thresholds) { + const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, threshold); + // console.log(`result for ${threshold}[${counter}]: ${result}`); + assertEquals(result, expectedAmounts[Object.keys(expectedAmounts)[counter]]); + counter++; + } + }, +}); From 941e8fdfb132c0b07f18e665ce9097667856f896 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 12:18:26 -0700 Subject: [PATCH 21/47] fix: update mining tests to use same model There are no changes to the function signatures between contracts, and new functions could be added with a comment line vs creating a whole new file. This also restructures the file so the functions are ordered the same as in the contract and cleans up the comments. --- .../ccd006-citycoin-mining-v2.model.ts | 109 ------------------ .../ccd006-citycoin-mining.model.ts | 56 ++++----- .../ccd006-citycoin-mining-v2.test.ts | 91 ++++++++------- 3 files changed, 74 insertions(+), 182 deletions(-) delete mode 100644 models/extensions/ccd006-citycoin-mining-v2.model.ts diff --git a/models/extensions/ccd006-citycoin-mining-v2.model.ts b/models/extensions/ccd006-citycoin-mining-v2.model.ts deleted file mode 100644 index a2049024..00000000 --- a/models/extensions/ccd006-citycoin-mining-v2.model.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; - -export enum ErrCode { - ERR_UNAUTHORIZED = 6000, - ERR_INVALID_CITY, - ERR_NO_ACTIVATION_DETAILS, - ERR_INACTIVE_CITY, - ERR_INVALID_USER, - ERR_INVALID_TREASURY, - ERR_INVALID_DELAY, - ERR_INVALID_COMMITS, - ERR_NOT_ENOUGH_FUNDS, - ERR_ALREADY_MINED, - ERR_REWARD_IMMATURE, - ERR_NO_VRF_SEED, - ERR_DID_NOT_MINE, - ERR_NO_MINER_DATA, - ERR_ALREADY_CLAIMED, - ERR_MINER_NOT_WINNER, - ERR_MINING_DISABLED, -} - -export class CCD006CityMiningV2 { - name: string; - static readonly ErrCode = ErrCode; - chain: Chain; - deployer: Account; - - constructor(chain: Chain, deployer: Account, name: string) { - this.name = name; - this.chain = chain; - this.deployer = deployer; - } - - // Authorization - - isDaoOrExtension(): ReadOnlyFn { - return this.callReadOnlyFn("is-dao-or-extension"); - } - - callback(sender: Account, memo: string) { - return Tx.contractCall(this.name, "callback", [types.principal(sender.address), types.buff(memo)], sender.address); - } - - // Internal DAO functions - - setRewardDelay(sender: Account, delay: number) { - return Tx.contractCall(this.name, "set-reward-delay", [types.uint(delay)], sender.address); - } - - setMiningEnabled(sender: Account, status: boolean) { - return Tx.contractCall(this.name, "set-mining-enabled", [types.bool(status)], sender.address); - } - - // Public functions - - mine(sender: Account, cityName: string, amounts: Array<number>) { - return Tx.contractCall(this.name, "mine", [types.ascii(cityName), types.list(amounts.map((entry) => types.uint(entry)))], sender.address); - } - - claimMiningReward(sender: Account, cityName: string, claimHeight: number) { - return Tx.contractCall(this.name, "claim-mining-reward", [types.ascii(cityName), types.uint(claimHeight)], sender.address); - } - - // Read only functions - - getRewardDelay(): ReadOnlyFn { - return this.callReadOnlyFn("get-reward-delay", []); - } - - getMiningStats(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-mining-stats", [types.uint(cityId), types.uint(blockHeight)]); - } - - hasMinedAtBlock(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { - return this.callReadOnlyFn("has-mined-at-block", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); - } - - getMiner(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { - return this.callReadOnlyFn("get-miner", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); - } - - getHighValue(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-high-value", [types.uint(cityId), types.uint(blockHeight)]); - } - - getBlockWinner(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-block-winner", [types.uint(cityId), types.uint(blockHeight)]); - } - - isBlockWinner(cityId: number, user: string, claimHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("is-block-winner", [types.uint(cityId), types.principal(user), types.uint(claimHeight)]); - } - - getCoinbaseAmount(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-coinbase-amount", [types.uint(cityId), types.uint(blockHeight)]); - } - - isMiningEnabled(): ReadOnlyFn { - return this.callReadOnlyFn("is-mining-enabled", []); - } - - // helper for calling read only functions - - private callReadOnlyFn(method: string, args: Array<any> = [], sender: Account = this.deployer): ReadOnlyFn { - const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); - return result; - } -} diff --git a/models/extensions/ccd006-citycoin-mining.model.ts b/models/extensions/ccd006-citycoin-mining.model.ts index 70651d53..7132d694 100644 --- a/models/extensions/ccd006-citycoin-mining.model.ts +++ b/models/extensions/ccd006-citycoin-mining.model.ts @@ -38,15 +38,11 @@ export class CCD006CityMining { return this.callReadOnlyFn("is-dao-or-extension"); } - // Internal DAO functions - - mine(sender: Account, cityName: string, amounts: Array<number>) { - return Tx.contractCall(this.name, "mine", [types.ascii(cityName), types.list(amounts.map((entry) => types.uint(entry)))], sender.address); + callback(sender: Account, memo: string) { + return Tx.contractCall(this.name, "callback", [types.principal(sender.address), types.buff(memo)], sender.address); } - claimMiningReward(sender: Account, cityName: string, claimHeight: number) { - return Tx.contractCall(this.name, "claim-mining-reward", [types.ascii(cityName), types.uint(claimHeight)], sender.address); - } + // Internal DAO functions setRewardDelay(sender: Account, delay: number) { return Tx.contractCall(this.name, "set-reward-delay", [types.uint(delay)], sender.address); @@ -56,50 +52,56 @@ export class CCD006CityMining { return Tx.contractCall(this.name, "set-mining-enabled", [types.bool(status)], sender.address); } - // Read only functions + // Public functions - isBlockWinner(cityId: number, user: string, claimHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("is-block-winner", [types.uint(cityId), types.principal(user), types.uint(claimHeight)]); + mine(sender: Account, cityName: string, amounts: Array<number>) { + return Tx.contractCall(this.name, "mine", [types.ascii(cityName), types.list(amounts.map((entry) => types.uint(entry)))], sender.address); } - getBlockWinner(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-block-winner", [types.uint(cityId), types.uint(blockHeight)]); + claimMiningReward(sender: Account, cityName: string, claimHeight: number) { + return Tx.contractCall(this.name, "claim-mining-reward", [types.ascii(cityName), types.uint(claimHeight)], sender.address); } - getHighValue(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-high-value", [types.uint(cityId), types.uint(blockHeight)]); + // Read only functions + + getRewardDelay(): ReadOnlyFn { + return this.callReadOnlyFn("get-reward-delay", []); } - getMinerAtBlock(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { - return this.callReadOnlyFn("get-miner", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); + getMiningStats(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-mining-stats", [types.uint(cityId), types.uint(blockHeight)]); } hasMinedAtBlock(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { return this.callReadOnlyFn("has-mined-at-block", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); } - getMiningStatsAtBlock(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-mining-stats", [types.uint(cityId), types.uint(blockHeight)]); + getMiner(cityId: number, blockHeight: number, userId: number): ReadOnlyFn { + return this.callReadOnlyFn("get-miner", [types.uint(cityId), types.uint(blockHeight), types.uint(userId)]); } - getCoinbaseAmount(cityId: number, blockHeight: number): ReadOnlyFn { - return this.callReadOnlyFn("get-coinbase-amount", [types.uint(cityId), types.uint(blockHeight)]); + getHighValue(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-high-value", [types.uint(cityId), types.uint(blockHeight)]); } - getRewardDelay(): ReadOnlyFn { - return this.callReadOnlyFn("get-reward-delay", []); + getBlockWinner(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-block-winner", [types.uint(cityId), types.uint(blockHeight)]); } - isMiningEnabled(): ReadOnlyFn { - return this.callReadOnlyFn("is-mining-enabled", []); + isBlockWinner(cityId: number, user: string, claimHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("is-block-winner", [types.uint(cityId), types.principal(user), types.uint(claimHeight)]); } - // Extension callback + getCoinbaseAmount(cityId: number, blockHeight: number): ReadOnlyFn { + return this.callReadOnlyFn("get-coinbase-amount", [types.uint(cityId), types.uint(blockHeight)]); + } - callback(sender: Account, memo: string) { - return Tx.contractCall(this.name, "callback", [types.principal(sender.address), types.buff(memo)], sender.address); + isMiningEnabled(): ReadOnlyFn { + return this.callReadOnlyFn("is-mining-enabled", []); } + // helper for calling read only functions + private callReadOnlyFn(method: string, args: Array<any> = [], sender: Account = this.deployer): ReadOnlyFn { const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); return result; diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts index 0bc20117..372f1348 100644 --- a/tests/extensions/ccd006-citycoin-mining-v2.test.ts +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -13,7 +13,6 @@ import { CCD002Treasury } from "../../models/extensions/ccd002-treasury.model.ts import { CCD003UserRegistry } from "../../models/extensions/ccd003-user-registry.model.ts"; import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.ts"; import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; -import { CCD006CityMiningV2 } from "../../models/extensions/ccd006-citycoin-mining-v2.model.ts"; import { CCD010CoreV2Adapter } from "../../models/extensions/ccd010-core-v2-adapter.model.ts"; import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; @@ -127,7 +126,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act @@ -143,7 +142,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const { receipts } = chain.mineBlock([ccd006CityMining.callback(sender, "test")]); @@ -163,7 +162,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [10, 10]; @@ -180,7 +179,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [10, 10]; @@ -202,7 +201,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [10, 10]; @@ -221,7 +220,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [10, 10]; @@ -241,7 +240,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [10, 10]; @@ -264,7 +263,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [100000000000001]; @@ -285,7 +284,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [50000000000000, 50000000000000]; @@ -307,7 +306,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [50000000000000, 50000000000000, 1]; @@ -329,7 +328,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries = [10, 10]; @@ -354,7 +353,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = []; @@ -378,7 +377,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10, 10, 10, 0, 10]; @@ -401,7 +400,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10]; @@ -454,7 +453,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const userId = 1; const commitAmount = 10; const numberOfBlocks = 200; @@ -512,7 +511,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!]; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const userIds = [1, 2, 3]; const commitAmount = 100; const numberOfBlocks = 100; @@ -581,7 +580,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const totalAmount = 30; const totalBlocks = 3; const userId = 1; @@ -625,7 +624,7 @@ Clarinet.test({ const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const ccd003UserRegistry = new CCD003UserRegistry(chain, sender, "ccd003-user-registry"); const totalAmount = 120; @@ -690,7 +689,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user1 = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); const totalAmount = 10; const totalBlocks = 1; @@ -751,7 +750,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user1 = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); const totalAmount = 10; const totalBlocks = 1; @@ -801,7 +800,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const { receipts } = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, 50)]); @@ -817,7 +816,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); @@ -836,7 +835,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); @@ -856,7 +855,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); @@ -878,7 +877,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); @@ -902,7 +901,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10]; @@ -929,7 +928,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10]; @@ -956,7 +955,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10]; @@ -999,7 +998,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10]; @@ -1045,7 +1044,7 @@ Clarinet.test({ const user1 = accounts.get("wallet_1")!; const user2 = accounts.get("wallet_2")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const totalAmount = 10; const totalBlocks = 1; const entries: number[] = [10]; @@ -1104,7 +1103,7 @@ Clarinet.test({ const user1 = accounts.get("wallet_1")!; const user2 = accounts.get("wallet_2")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const totalAmount = 10; const totalBlocks = 1; const entries = [10]; @@ -1202,7 +1201,7 @@ Clarinet.test({ const user1 = accounts.get("wallet_1")!; const user2 = accounts.get("wallet_2")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); gt.getBalance(user1.address).result.expectOk().expectUint(0); gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); @@ -1250,7 +1249,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user1 = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); @@ -1288,7 +1287,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const { receipts } = chain.mineBlock([ccd006CityMining.setRewardDelay(sender, 50)]); @@ -1304,7 +1303,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_004); @@ -1320,7 +1319,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); ccd006CityMining.getRewardDelay().result.expectUint(100); // act @@ -1344,7 +1343,7 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const entries: number[] = [10]; @@ -1379,7 +1378,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const user = accounts.get("wallet_1")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); @@ -1405,7 +1404,7 @@ Clarinet.test({ const user1 = accounts.get("wallet_1")!; const user2 = accounts.get("wallet_2")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const entries = [10]; const totalAmount = entries.reduce((a, b) => a + b, 0); const totalBlocks = entries.length; @@ -1495,7 +1494,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const block = chain.mineBlock([ccd006CityMining.setMiningEnabled(sender, true)]); // assert @@ -1508,7 +1507,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act // assert ccd006CityMining.isMiningEnabled().result.expectBool(true); @@ -1524,7 +1523,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 100); @@ -1539,7 +1538,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // get MIA/NYC city IDs constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); // set city status to activated @@ -1566,7 +1565,7 @@ Clarinet.test({ fn(chain: Chain, accounts: Map<string, Account>) { // arrange const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // get MIA/NYC city IDs constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); // set city status to activated @@ -1595,7 +1594,7 @@ Clarinet.test({ // arrange const sender = accounts.get("deployer")!; const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // get MIA/NYC city IDs constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); // set city activation details From 773475d8c5926aad80744fdab3adaf0e5d41566a Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Tue, 23 May 2023 12:27:10 -0700 Subject: [PATCH 22/47] fix: clone test proposals and update utils --- Clarinet.toml | 15 +++++ .../test-ccd006-citycoin-mining-v2-001.clar | 14 +++++ .../test-ccd006-citycoin-mining-v2-002.clar | 13 ++++ .../test-ccd006-citycoin-mining-v2-003.clar | 13 ++++ .../test-ccd006-citycoin-mining-v2-004.clar | 13 ++++ .../test-ccd006-citycoin-mining-v2-005.clar | 9 +++ .../ccd006-citycoin-mining-v2.test.ts | 59 ++++++++++--------- utils/common.ts | 6 ++ 8 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-001.clar create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-003.clar create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-004.clar create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-005.clar diff --git a/Clarinet.toml b/Clarinet.toml index 1bf7aeaf..3aa4e121 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -350,6 +350,21 @@ path = "tests/contracts/proposals/test-ccd006-citycoin-mining-004.clar" [contracts.test-ccd006-citycoin-mining-005] path = "tests/contracts/proposals/test-ccd006-citycoin-mining-005.clar" +[contracts.test-ccd006-citycoin-mining-v2-001] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-001.clar" + +[contracts.test-ccd006-citycoin-mining-v2-002] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-002.clar" + +[contracts.test-ccd006-citycoin-mining-v2-003] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-003.clar" + +[contracts.test-ccd006-citycoin-mining-v2-004] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-004.clar" + +[contracts.test-ccd006-citycoin-mining-v2-005] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-005.clar" + [contracts.test-ccd007-citycoin-stacking-001] path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-001.clar" diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-001.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-001.clar new file mode 100644 index 00000000..32cac335 --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-001.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd005-city-data add-treasury u1 .mia-treasury "mining")) + (try! (contract-call? .base-dao set-extension .ccd006-citycoin-mining-v2 false)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar new file mode 100644 index 00000000..7cd8afc2 --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar @@ -0,0 +1,13 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining-v2 "mining-v2")) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-003.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-003.clar new file mode 100644 index 00000000..30edd013 --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-003.clar @@ -0,0 +1,13 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd006-citycoin-mining-v2 set-reward-delay u50)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-004.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-004.clar new file mode 100644 index 00000000..3f2c89fd --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-004.clar @@ -0,0 +1,13 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd006-citycoin-mining-v2 set-reward-delay u0)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-005.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-005.clar new file mode 100644 index 00000000..d38690e2 --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-005.clar @@ -0,0 +1,9 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled false) +) diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts index 372f1348..1a852aea 100644 --- a/tests/extensions/ccd006-citycoin-mining-v2.test.ts +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -247,7 +247,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_001); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); @@ -270,7 +270,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); // assert @@ -291,7 +291,8 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); + passProposal(chain, accounts, PROPOSALS.CCIP_014); const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); // assert @@ -313,7 +314,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); // assert @@ -335,7 +336,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_003); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); @@ -360,7 +361,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); @@ -384,7 +385,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); @@ -409,7 +410,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); @@ -467,7 +468,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); @@ -525,7 +526,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const block = chain.mineBlock([ccd006CityMining.mine(users[0], mia.cityName, entries), ccd006CityMining.mine(users[1], mia.cityName, entries), ccd006CityMining.mine(users[2], mia.cityName, entries)]); const firstBlock = block.height - 1; @@ -590,7 +591,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries), ccd006CityMining.mine(sender, mia.cityName, entries)]); const firstBlock = block.height - 1; const lastBlock = firstBlock + entries.length - 1; @@ -635,7 +636,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); const block = chain.mineBlock([ccd006CityMining.mine(users[0], mia.cityName, entries), ccd006CityMining.mine(users[1], mia.cityName, entries), ccd006CityMining.mine(users[2], mia.cityName, entries), ccd006CityMining.mine(users[3], mia.cityName, entries)]); const firstBlock = block.height - 1; const lastBlock = firstBlock + entries.length - 1; @@ -704,7 +705,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); @@ -765,7 +766,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); @@ -908,7 +909,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); let block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); const claimHeight = block.height - 1; // one less than actual bh @@ -935,7 +936,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); const claimHeight = block.height - 1; // one less than actual bh @@ -962,7 +963,7 @@ Clarinet.test({ constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); block.receipts[0].result.expectOk().expectBool(true); @@ -1006,7 +1007,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_007); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); block.receipts[0].result.expectOk().expectBool(true); @@ -1054,7 +1055,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_007); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const block1 = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); @@ -1115,7 +1116,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); @@ -1214,7 +1215,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); // add mining treasury - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); // assert @@ -1259,7 +1260,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); // add mining treasury - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); // assert @@ -1306,7 +1307,7 @@ Clarinet.test({ const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); // act - const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_004); + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_004); // assert ccd006CityMining.getRewardDelay().result.expectUint(100); @@ -1323,7 +1324,7 @@ Clarinet.test({ ccd006CityMining.getRewardDelay().result.expectUint(100); // act - const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_003); + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_003); // assert ccd006CityMining.getRewardDelay().result.expectUint(50); @@ -1352,8 +1353,8 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); @@ -1385,7 +1386,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); const claimHeight = 6; // one less than actual bh chain.mineEmptyBlock(rewardDelay); const block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); @@ -1416,7 +1417,7 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); @@ -1426,7 +1427,7 @@ Clarinet.test({ chain.mineEmptyBlock(rewardDelay + 1); // disable mining - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); const miningClaimUser1 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); const miningClaimUser2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); diff --git a/utils/common.ts b/utils/common.ts index 9af02cd7..bfc365de 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -30,6 +30,7 @@ export const EXTENSIONS = { export const PROPOSALS = { CCIP_012: ADDRESS.concat(".ccip012-bootstrap"), CCIP_013: ADDRESS.concat(".ccip013-migration"), + CCIP_014: ADDRESS.concat(".ccip014-pox-3"), 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"), @@ -74,6 +75,11 @@ export const PROPOSALS = { TEST_CCD006_CITY_MINING_003: ADDRESS.concat(".test-ccd006-citycoin-mining-003"), TEST_CCD006_CITY_MINING_004: ADDRESS.concat(".test-ccd006-citycoin-mining-004"), TEST_CCD006_CITY_MINING_005: ADDRESS.concat(".test-ccd006-citycoin-mining-005"), + TEST_CCD006_CITY_MINING_V2_001: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-001"), + TEST_CCD006_CITY_MINING_V2_002: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-002"), + TEST_CCD006_CITY_MINING_V2_003: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-003"), + TEST_CCD006_CITY_MINING_V2_004: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-004"), + TEST_CCD006_CITY_MINING_V2_005: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-005"), TEST_CCD007_CITY_STACKING_001: ADDRESS.concat(".test-ccd007-citycoin-stacking-001"), TEST_CCD007_CITY_STACKING_002: ADDRESS.concat(".test-ccd007-citycoin-stacking-002"), TEST_CCD007_CITY_STACKING_003: ADDRESS.concat(".test-ccd007-citycoin-stacking-003"), From 01fdf6245bef2c46202ae1e654c091604d7c5928 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 24 May 2023 12:32:15 -0700 Subject: [PATCH 23/47] fix: remove checks for before vote starts This vote will start as soon as the contract is deployed, and left comments in case it needs to be used in later versions. --- contracts/proposals/ccip014-pox-3.clar | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 095641ae..8865bdc6 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -101,6 +101,7 @@ ) ) +;; TODO: use at-block for call below? (define-public (vote-on-ccip014 (vote bool)) (let ( @@ -110,10 +111,12 @@ (voterRecord (map-get? UserVotes voterId)) ) ;; check that proposal is active - (asserts! (and - (>= block-height (var-get voteStart)) - (<= block-height (var-get voteEnd))) - ERR_PROPOSAL_NOT_ACTIVE) + ;;(asserts! (and + ;; (>= block-height (var-get voteStart)) + ;; (<= block-height (var-get voteEnd))) + ;; ERR_PROPOSAL_NOT_ACTIVE) + ;; lines above modified since vote will start at deployed height + (asserts! (<= block-height (var-get voteEnd)) ERR_PROPOSAL_NOT_ACTIVE) ;; check if vote record exists (match voterRecord record ;; if the voterRecord exists @@ -143,7 +146,6 @@ ;; if the voterRecord does not exist (let ( - ;; TODO (scaledVoteMia (default-to u0 (get-mia-vote miaId voterId true))) (scaledVoteNyc (default-to u0 (get-nyc-vote nycId voterId true))) (voteMia (scale-down scaledVoteMia)) @@ -182,7 +184,8 @@ (define-read-only (is-executable) (begin - (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) + ;; line below removed since vote will start at deployed height + ;; (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) (asserts! (> (var-get yesTotal) (var-get noTotal)) ERR_VOTE_FAILED) (ok true) From 866d44d9a270971ba4906b4ddbaff13900f916ef Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 24 May 2023 12:37:45 -0700 Subject: [PATCH 24/47] fix: add trait for ccip-011 vote function This renames the main function vote-on-ccip014 to the generic vote-on-proposal so it can be reused between contracts. --- Clarinet.toml | 3 +++ contracts/proposals/ccip014-pox-3.clar | 3 ++- contracts/traits/ccip-011-trait.clar | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 contracts/traits/ccip-011-trait.clar diff --git a/Clarinet.toml b/Clarinet.toml index 3aa4e121..603684e1 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -168,6 +168,9 @@ path = "contracts/traits/ccd006-trait.clar" [contracts.ccd007-trait] path = "contracts/traits/ccd007-trait.clar" +[contracts.ccip-011-trait] +path = "contracts/traits/ccip-011-trait.clar" + # CITYCOINS EXTERNAL CONTRACTS [contracts.citycoin-vrf-v2] diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 8865bdc6..31ae09b4 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -1,6 +1,7 @@ ;; TRAITS (impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip-011-trait.ccip-011-trait) ;; ERRORS @@ -102,7 +103,7 @@ ) ;; TODO: use at-block for call below? -(define-public (vote-on-ccip014 (vote bool)) +(define-public (vote-on-proposal (vote bool)) (let ( (miaId (unwrap! (contract-call? .ccd004-city-registry get-city-id "mia") ERR_NO_CITY_ID)) diff --git a/contracts/traits/ccip-011-trait.clar b/contracts/traits/ccip-011-trait.clar new file mode 100644 index 00000000..2ceb15bf --- /dev/null +++ b/contracts/traits/ccip-011-trait.clar @@ -0,0 +1,5 @@ +(define-trait ccip-011-trait + ( + (vote-on-proposal (bool) (response bool uint)) + ) +) From 2f349c8d0bdf9715a11e15d8c01be7c2f356f901 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 24 May 2023 15:40:16 -0700 Subject: [PATCH 25/47] fix: update ccip-011 ref to ccip-015 Also updates the trait to match the SIP text. --- Clarinet.toml | 4 ++-- contracts/proposals/ccip014-pox-3.clar | 2 +- contracts/traits/ccip-011-trait.clar | 5 ----- contracts/traits/ccip-015-trait.clar | 6 ++++++ 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 contracts/traits/ccip-011-trait.clar create mode 100644 contracts/traits/ccip-015-trait.clar diff --git a/Clarinet.toml b/Clarinet.toml index 603684e1..a870e4c8 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -168,8 +168,8 @@ path = "contracts/traits/ccd006-trait.clar" [contracts.ccd007-trait] path = "contracts/traits/ccd007-trait.clar" -[contracts.ccip-011-trait] -path = "contracts/traits/ccip-011-trait.clar" +[contracts.ccip-015-trait] +path = "contracts/traits/ccip-015-trait.clar" # CITYCOINS EXTERNAL CONTRACTS diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 31ae09b4..d95fdb2f 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -1,7 +1,7 @@ ;; TRAITS (impl-trait .proposal-trait.proposal-trait) -(impl-trait .ccip-011-trait.ccip-011-trait) +(impl-trait .ccip-015-trait.ccip-015-trait) ;; ERRORS diff --git a/contracts/traits/ccip-011-trait.clar b/contracts/traits/ccip-011-trait.clar deleted file mode 100644 index 2ceb15bf..00000000 --- a/contracts/traits/ccip-011-trait.clar +++ /dev/null @@ -1,5 +0,0 @@ -(define-trait ccip-011-trait - ( - (vote-on-proposal (bool) (response bool uint)) - ) -) diff --git a/contracts/traits/ccip-015-trait.clar b/contracts/traits/ccip-015-trait.clar new file mode 100644 index 00000000..56da3c9e --- /dev/null +++ b/contracts/traits/ccip-015-trait.clar @@ -0,0 +1,6 @@ +(define-trait ccip-015-trait + ( + (vote-on-proposal (bool) (response bool uint)) + (is-executable () (response bool uint)) + ) +) From e013b982d0156f2b30e3e0f28ddf7af3c49d078d Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 25 May 2023 16:46:09 -0700 Subject: [PATCH 26/47] fix: minor cleanup/changes --- contracts/proposals/ccip014-pox-3.clar | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index d95fdb2f..70948d34 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -17,9 +17,9 @@ ;; CONSTANTS (define-constant CCIP_014 { - name: "", - link: "", - hash: "", + name: "Upgrade to pox-3", + link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", + hash: "TBD", }) (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places @@ -64,7 +64,7 @@ (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) ) - ;; check vote details + ;; check vote complete/passed (try! (is-executable)) ;; enable mining v2 treasuries in the DAO @@ -72,6 +72,7 @@ (list {extension: .ccd002-treasury-mia-mining-v2, enabled: true} {extension: .ccd002-treasury-nyc-mining-v2, enabled: true} + {extension: .ccd006-citycoin-mining-v2, enabled: true} ) )) @@ -99,10 +100,9 @@ (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) (ok true) - ) + ) ) -;; TODO: use at-block for call below? (define-public (vote-on-proposal (vote bool)) (let ( @@ -185,9 +185,10 @@ (define-read-only (is-executable) (begin - ;; line below removed since vote will start at deployed height + ;; line below revised since vote will start at deployed height ;; (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) - (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) + ;; line below revised since vote will end when proposal executes + ;; (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) (asserts! (> (var-get yesTotal) (var-get noTotal)) ERR_VOTE_FAILED) (ok true) ) From 5f12ade9df7c5baaac66a44bffc7a415a2f5dbc5 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 25 May 2023 16:59:19 -0700 Subject: [PATCH 27/47] fix: add var to track vote active --- contracts/proposals/ccip014-pox-3.clar | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 70948d34..b49335c3 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -31,8 +31,10 @@ ;; 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 @@ -67,6 +69,10 @@ ;; check vote complete/passed (try! (is-executable)) + ;; update vote variables + (var-set voteEnd block-height) + (var-set voteActive false) + ;; enable mining v2 treasuries in the DAO (try! (contract-call? .base-dao set-extensions (list @@ -116,8 +122,7 @@ ;; (>= block-height (var-get voteStart)) ;; (<= block-height (var-get voteEnd))) ;; ERR_PROPOSAL_NOT_ACTIVE) - ;; lines above modified since vote will start at deployed height - (asserts! (<= block-height (var-get voteEnd)) ERR_PROPOSAL_NOT_ACTIVE) + (asserts! (var-get voteActive) ERR_PROPOSAL_NOT_ACTIVE) ;; check if vote record exists (match voterRecord record ;; if the voterRecord exists @@ -185,15 +190,23 @@ (define-read-only (is-executable) (begin + ;; additional checks could be added here in future proposals ;; line below revised since vote will start at deployed height ;; (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) ;; line below revised since vote will end when proposal executes ;; (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) + ;; 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_014) ) From 6b0ae28e4dfb649ab47ec3033e1eb073165825f0 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Thu, 25 May 2023 17:15:05 -0700 Subject: [PATCH 28/47] fix: add idea for test abstraction --- tests/extensions/ccd006-citycoin-mining-v2.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts index 1a852aea..f8e5989f 100644 --- a/tests/extensions/ccd006-citycoin-mining-v2.test.ts +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -189,6 +189,11 @@ Clarinet.test({ passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); // set city treasury (fails before this check) passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_012); + // generalize to three functions: + // passCCIP014 which will vote and execute + // voteCCIP014 which votes for given user + // execCCIP014 which executes the proposal + console.log(passProposal(chain, accounts, PROPOSALS.CCIP_014)); const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); // assert From 62fb43dc94f6ac253bbf9e09f7a30a9067a48b01 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 26 May 2023 17:12:20 -0700 Subject: [PATCH 29/47] fix: prep model and common for ccip014 tests --- models/proposals/ccip014-pox-3.model.ts | 72 +++++++++++++++++++ .../ccd006-citycoin-mining-v2.test.ts | 2 + utils/common.ts | 1 + 3 files changed, 75 insertions(+) create mode 100644 models/proposals/ccip014-pox-3.model.ts diff --git a/models/proposals/ccip014-pox-3.model.ts b/models/proposals/ccip014-pox-3.model.ts new file mode 100644 index 00000000..9906103c --- /dev/null +++ b/models/proposals/ccip014-pox-3.model.ts @@ -0,0 +1,72 @@ +import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 1400, + ERR_VOTED_ALREADY, + ERR_NOTHING_STACKED, + ERR_USER_NOT_FOUND, + ERR_PROPOSAL_NOT_ACTIVE, + ERR_PROPOSAL_STILL_ACTIVE, + ERR_NO_CITY_ID, + ERR_VOTE_VAILED, +} + +export class CCIP014 { + name = "ccip014-pox-3"; + static readonly ErrCode = ErrCode; + chain: Chain; + deployer: Account; + + constructor(chain: Chain, deployer: Account) { + this.chain = chain; + this.deployer = deployer; + } + + // public functions + + // execute excluded since can be used with 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() { + return this.callReadOnlyFn("get-voter-info"); + } + + 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<any> = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts index f8e5989f..cfab8ce9 100644 --- a/tests/extensions/ccd006-citycoin-mining-v2.test.ts +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -15,6 +15,8 @@ import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.t import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; import { CCD010CoreV2Adapter } from "../../models/extensions/ccd010-core-v2-adapter.model.ts"; import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; +import { CCD001DirectExecute } from "../../models/extensions/ccd001-direct-execute.model.js"; +import { CCIP014 } from "../../models/proposals/ccip014-pox-3.model.js"; // ============================= // INTERNAL DATA / FUNCTIONS diff --git a/utils/common.ts b/utils/common.ts index bfc365de..6c2b2195 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -21,6 +21,7 @@ export const EXTENSIONS = { CCD004_CITY_REGISTRY: ADDRESS.concat(".ccd004-city-registry"), CCD005_CITY_DATA: ADDRESS.concat(".ccd005-city-data"), CCD006_CITYCOIN_MINING: ADDRESS.concat(".ccd006-citycoin-mining"), + CCD006_CITYCOIN_MINING_V2: ADDRESS.concat(".ccd006-citycoin-mining-v2"), CCD007_CITYCOIN_STACKING: ADDRESS.concat(".ccd007-citycoin-stacking"), CCD008_CITY_ACTIVATION: ADDRESS.concat(".ccd008-city-activation"), CCD009_AUTH_V2_ADAPTER: ADDRESS.concat(".ccd009-auth-v2-adapter"), From 9b3e4f34587a030d85286ebf48328da631ed6679 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 26 May 2023 17:55:41 -0700 Subject: [PATCH 30/47] fix: add initial ccip-014 tests Vote value for 500 Stacked CityCoins is showing 43 instead of 438 for MIA. --- models/proposals/ccip014-pox-3.model.ts | 4 +- .../ccd006-citycoin-mining-v2.test.ts | 62 +++---------- tests/proposals/ccip014-pox-3.test.ts | 87 +++++++++++++++++++ utils/common.ts | 35 ++++++++ 4 files changed, 137 insertions(+), 51 deletions(-) create mode 100644 tests/proposals/ccip014-pox-3.test.ts diff --git a/models/proposals/ccip014-pox-3.model.ts b/models/proposals/ccip014-pox-3.model.ts index 9906103c..24c72598 100644 --- a/models/proposals/ccip014-pox-3.model.ts +++ b/models/proposals/ccip014-pox-3.model.ts @@ -8,10 +8,10 @@ enum ErrCode { ERR_PROPOSAL_NOT_ACTIVE, ERR_PROPOSAL_STILL_ACTIVE, ERR_NO_CITY_ID, - ERR_VOTE_VAILED, + ERR_VOTE_FAILED, } -export class CCIP014 { +export class CCIP014Pox3 { name = "ccip014-pox-3"; static readonly ErrCode = ErrCode; chain: Chain; diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts index cfab8ce9..f1b9bed3 100644 --- a/tests/extensions/ccd006-citycoin-mining-v2.test.ts +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -8,15 +8,14 @@ * 5. read-only functions */ import { Account, assert, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; -import { constructAndPassProposal, EXTENSIONS, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { constructAndPassProposal, EXTENSIONS, mia, passProposal, PROPOSALS } from "../../utils/common.ts"; import { CCD002Treasury } from "../../models/extensions/ccd002-treasury.model.ts"; import { CCD003UserRegistry } from "../../models/extensions/ccd003-user-registry.model.ts"; import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.ts"; import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; import { CCD010CoreV2Adapter } from "../../models/extensions/ccd010-core-v2-adapter.model.ts"; import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; -import { CCD001DirectExecute } from "../../models/extensions/ccd001-direct-execute.model.js"; -import { CCIP014 } from "../../models/proposals/ccip014-pox-3.model.js"; +import { CCIP014 } from "../../models/proposals/ccip014-pox-3.model.ts"; // ============================= // INTERNAL DATA / FUNCTIONS @@ -24,41 +23,6 @@ import { CCIP014 } from "../../models/proposals/ccip014-pox-3.model.js"; const rewardDelay = 100; -// reusable city data - -type CityData = { - cityId: number; - cityName: string; - treasuryV1Contract: string; - treasuryV1Id: number; - treasuryV1Name: string; - treasuryV2Contract: string; - treasuryV2Id: number; - treasuryV2Name: string; -}; - -const mia: CityData = { - cityId: 1, - cityName: "mia", - treasuryV1Contract: "ccd002-treasury-mia-mining", - treasuryV1Id: 1, - treasuryV1Name: "mining", - treasuryV2Contract: "ccd002-treasury-mia-mining-v2", - treasuryV2Id: 2, - treasuryV2Name: "mining-v2", -}; - -const nyc: CityData = { - cityId: 2, - cityName: "nyc", - treasuryV1Contract: "ccd002-treasury-nyc-mining", - treasuryV1Id: 1, - treasuryV1Name: "mining", - treasuryV2Contract: "ccd002-treasury-nyc-mining-v2", - treasuryV2Id: 2, - treasuryV2Name: "mining-v2", -}; - // reusable mining functions const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, userId: number, miningStatsAt: any, minerAt: any) => { @@ -67,7 +31,7 @@ const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, claimed: types.bool(miningStatsAt.claimed), miners: types.uint(miningStatsAt.miners), }; - assertEquals(ccd006CityMining.getMiningStatsAtBlock(cityId, height).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiningStats(cityId, height).result.expectTuple(), expectedStats); expectedStats = { commit: types.uint(minerAt.commit), @@ -75,7 +39,7 @@ const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, low: types.uint(minerAt.low), winner: types.bool(minerAt.winner), }; - assertEquals(ccd006CityMining.getMinerAtBlock(cityId, height, userId).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiner(cityId, height, userId).result.expectTuple(), expectedStats); }; const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006CityMining, chain: Chain, sender: Account): any => { @@ -97,7 +61,7 @@ const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006C /** console.log("getCoinbaseAmount : " + coinbase) console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(mia.cityId, user1.address, claimHeight).result.expectSome().expectTuple()) - console.log("getMiningStatsAtBlock : ", ccd006CityMining.getMiningStatsAtBlock(mia.cityId, claimHeight)) + console.log("getMiningStats : ", ccd006CityMining.getMiningStats(mia.cityId, claimHeight)) */ winner = 1; } else if (miningClaimBlock.receipts[1].result === "(ok true)") { @@ -110,7 +74,7 @@ const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006C /** console.log("getCoinbaseAmount : " + coinbase) console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(mia.cityId, user2.address, claimHeight).result.expectSome().expectTuple()) - console.log("getMiningStatsAtBlock : ", ccd006CityMining.getMiningStatsAtBlock(mia.cityId, claimHeight)) + console.log("getMiningStats : ", ccd006CityMining.getMiningStats(mia.cityId, claimHeight)) */ } else { console.log("======== NOONE WINS ========================="); @@ -440,13 +404,13 @@ Clarinet.test({ low: types.uint(0), winner: types.bool(false), }; - assertEquals(ccd006CityMining.getMinerAtBlock(mia.cityId, firstBlock, userId).result.expectTuple(), expectedStats2); + assertEquals(ccd006CityMining.getMiner(mia.cityId, firstBlock, userId).result.expectTuple(), expectedStats2); const expectedStats = { amount: types.uint(10), claimed: types.bool(false), miners: types.uint(1), }; - assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedStats); ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); @@ -501,8 +465,8 @@ Clarinet.test({ winner: types.bool(false), }; for (let i = 0; i < entries.length; i++) { - assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedStats); - assertEquals(ccd006CityMining.getMinerAtBlock(mia.cityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); + assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiner(mia.cityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); } ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); @@ -556,7 +520,7 @@ Clarinet.test({ }; // loop through each block to check miner stats for (let i = 0; i < entries.length; i++) { - assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedStats); // loop through each user for (let j = 0; j < userIds.length; j++) { // check the data @@ -568,7 +532,7 @@ Clarinet.test({ low: types.uint(j === 0 ? 0 : lastCommit + 1), winner: types.bool(false), }; - assertEquals(ccd006CityMining.getMinerAtBlock(mia.cityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); + assertEquals(ccd006CityMining.getMiner(mia.cityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); } } @@ -1376,7 +1340,7 @@ Clarinet.test({ claimed: types.bool(false), miners: types.uint(0), }; - assertEquals(ccd006CityMining.getMiningStatsAtBlock(mia.cityId, firstBlock).result.expectTuple(), expectedMiningStats); + assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedMiningStats); }, }); diff --git a/tests/proposals/ccip014-pox-3.test.ts b/tests/proposals/ccip014-pox-3.test.ts new file mode 100644 index 00000000..a7646857 --- /dev/null +++ b/tests/proposals/ccip014-pox-3.test.ts @@ -0,0 +1,87 @@ +import { Account, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { constructAndPassProposal, EXTENSIONS, mia, nyc, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { CCD007CityStacking } from "../../models/extensions/ccd007-citycoin-stacking.model.ts"; +import { CCIP014Pox3 } from "../../models/proposals/ccip014-pox-3.model.ts"; +import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; + +Clarinet.test({ + name: "ccip-014: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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-014 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() fails with ERR_VOTE_FAILED if there are more no than yes votes", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + const ccip014pox3 = new CCIP014Pox3(chain, sender); + + const amountStacked = 500; + const lockPeriod = 10; + + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_STACKING).result.expectOk().expectUint(0); + // 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 4 + // votes are counted in cycles 2-3 + console.log(chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10)); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 1)); + console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 2)); + + // execute two no votes + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, false), ccip014pox3.voteOnProposal(user2, false)]); + + console.log(`voting block:\n${JSON.stringify(votingBlock, null, 2)}`); + + // act + + // execute ccip-014 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_VOTE_FAILED); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index 6c2b2195..c59ff307 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -131,3 +131,38 @@ export const constructAndPassProposal = (chain: Chain, accounts: Map<string, Acc // console.log(`block:\n${JSON.stringify(block, null, 2)}`); return block; }; + +// reusable city data + +export type CityData = { + cityId: number; + cityName: string; + treasuryV1Contract: string; + treasuryV1Id: number; + treasuryV1Name: string; + treasuryV2Contract: string; + treasuryV2Id: number; + treasuryV2Name: string; +}; + +export const mia: CityData = { + cityId: 1, + cityName: "mia", + treasuryV1Contract: "ccd002-treasury-mia-mining", + treasuryV1Id: 1, + treasuryV1Name: "mining", + treasuryV2Contract: "ccd002-treasury-mia-mining-v2", + treasuryV2Id: 2, + treasuryV2Name: "mining-v2", +}; + +export const nyc: CityData = { + cityId: 2, + cityName: "nyc", + treasuryV1Contract: "ccd002-treasury-nyc-mining", + treasuryV1Id: 1, + treasuryV1Name: "mining", + treasuryV2Contract: "ccd002-treasury-nyc-mining-v2", + treasuryV2Id: 2, + treasuryV2Name: "mining-v2", +}; From e58dff1dfbc1c2a64c426d50507d611311dd48af Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Fri, 26 May 2023 19:10:35 -0700 Subject: [PATCH 31/47] fix: add ccip-014 tests, fixes #62 --- Clarinet.toml | 3 + contracts/proposals/ccip014-pox-3.clar | 2 +- models/proposals/ccip014-pox-3.model.ts | 4 +- .../proposals/test-ccip014-pox-3-001.clar | 40 ++ tests/proposals/ccip014-pox-3.test.ts | 442 +++++++++++++++++- utils/common.ts | 1 + 6 files changed, 479 insertions(+), 13 deletions(-) create mode 100644 tests/contracts/proposals/test-ccip014-pox-3-001.clar diff --git a/Clarinet.toml b/Clarinet.toml index a870e4c8..4f2210c4 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -407,6 +407,9 @@ path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-012.clar" [contracts.test-ccd011-stacking-payouts-001] path = "tests/contracts/proposals/test-ccd011-stacking-payouts-001.clar" +[contracts.test-ccip014-pox-3-001] +path = "tests/contracts/proposals/test-ccip014-pox-3-001.clar" + [repl] costs_version = 2 parser_version = 2 diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index b49335c3..9e71cc8c 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -24,7 +24,7 @@ (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 u876) ;; 0.876 or 87.6% +(define-constant MIA_SCALE_FACTOR u8760) ;; 0.8760 or 87.60% ;; MIA votes scaled to make 1 MIA = 1 NYC ;; full calculation available in CCIP-014 diff --git a/models/proposals/ccip014-pox-3.model.ts b/models/proposals/ccip014-pox-3.model.ts index 24c72598..0c805b52 100644 --- a/models/proposals/ccip014-pox-3.model.ts +++ b/models/proposals/ccip014-pox-3.model.ts @@ -52,8 +52,8 @@ export class CCIP014Pox3 { return this.callReadOnlyFn("get-vote-totals"); } - getVoterInfo() { - return this.callReadOnlyFn("get-voter-info"); + getVoterInfo(userId: number) { + return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]); } getMiaVote(cityId: number, userId: number, scaled: boolean) { diff --git a/tests/contracts/proposals/test-ccip014-pox-3-001.clar b/tests/contracts/proposals/test-ccip014-pox-3-001.clar new file mode 100644 index 00000000..31c09835 --- /dev/null +++ b/tests/contracts/proposals/test-ccip014-pox-3-001.clar @@ -0,0 +1,40 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Sets up everything required for CCIP-014 + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; test-ccd004-city-registry-001 + (try! (contract-call? .ccd004-city-registry get-or-create-city-id "mia")) + (try! (contract-call? .ccd004-city-registry get-or-create-city-id "nyc")) + ;; test-ccd005-city-data-001 + (try! (contract-call? .ccd005-city-data set-activation-details u1 u1 u1 u5 u1)) + (try! (contract-call? .ccd005-city-data set-activation-details u2 u2 u2 u2 u2)) + ;; test-ccd005-city-data-002 + (try! (contract-call? .ccd005-city-data set-activation-status u1 true)) + (try! (contract-call? .ccd005-city-data set-activation-status u2 true)) + ;; test-ccd006-city-mining-002 + nyc + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-mining "mining")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-mining "mining")) + ;; test-ccd007-city-stacking-007 + nyc + (try! (contract-call? .ccd005-city-data add-treasury u1 .ccd002-treasury-mia-stacking "stacking")) + (try! (contract-call? .ccd005-city-data add-treasury u2 .ccd002-treasury-nyc-stacking "stacking")) + ;; test-ccd007-city-stacking-009 + nyc + (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-nyc mint u1000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)) + (try! (contract-call? .test-ccext-governance-token-nyc mint u1000 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG)) + ;; test-ccd007-city-stacking-010 + nyc + (try! (contract-call? .ccd002-treasury-mia-stacking set-allowed .test-ccext-governance-token-mia true)) + (try! (contract-call? .ccd002-treasury-nyc-stacking set-allowed .test-ccext-governance-token-nyc true)) + (ok true) + ) +) + +;; setup each passed proposal here +;; for more fine grained control +;; and ability to use both cities \ No newline at end of file diff --git a/tests/proposals/ccip014-pox-3.test.ts b/tests/proposals/ccip014-pox-3.test.ts index a7646857..d9b187ac 100644 --- a/tests/proposals/ccip014-pox-3.test.ts +++ b/tests/proposals/ccip014-pox-3.test.ts @@ -1,8 +1,8 @@ -import { Account, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; -import { constructAndPassProposal, EXTENSIONS, mia, nyc, passProposal, PROPOSALS } from "../../utils/common.ts"; +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 { CCIP014Pox3 } from "../../models/proposals/ccip014-pox-3.model.ts"; -import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; Clarinet.test({ name: "ccip-014: execute() fails with ERR_VOTE_FAILED if there are no votes", @@ -34,14 +34,11 @@ Clarinet.test({ const user1 = accounts.get("wallet_1")!; const user2 = accounts.get("wallet_2")!; const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); - const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); const ccip014pox3 = new CCIP014Pox3(chain, sender); const amountStacked = 500; const lockPeriod = 10; - gt.getBalance(user1.address).result.expectOk().expectUint(0); - gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_STACKING).result.expectOk().expectUint(0); // progress the chain to avoid underflow in // stacking reward cycle calculation chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); @@ -65,23 +62,448 @@ Clarinet.test({ // progress the chain to cycle 4 // votes are counted in cycles 2-3 - console.log(chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10)); + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); - console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 1)); - console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 2)); + // act // execute two no votes const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, false), ccip014pox3.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(ccip014pox3.getVoterInfo(1)); + console.log(ccip014pox3.getMiaVote(mia.cityId, 1, false)); + console.log(ccip014pox3.getMiaVote(mia.cityId, 1, true)); + console.log("user 2:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, 2, 2)); + console.log(ccip014pox3.getVoterInfo(2)); + console.log(ccip014pox3.getMiaVote(mia.cityId, 2, false)); + console.log(ccip014pox3.getMiaVote(mia.cityId, 2, true)); + */ + + // execute ccip-014 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there is a single yes vote", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); // act + // execute single yes vote + const votingBlock = chain.mineBlock([ccip014pox3.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(ccip014pox3.getVoterInfo(userId)); + console.log(ccip014pox3.getMiaVote(mia.cityId, userId, false)); + console.log(ccip014pox3.getMiaVote(mia.cityId, userId, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, userId)); + console.log(ccip014pox3.getVoterInfo(userId)); + console.log(ccip014pox3.getNycVote(nyc.cityId, userId, false)); + console.log(ccip014pox3.getNycVote(nyc.cityId, userId, true)); + */ + // execute ccip-014 const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); // assert - block.receipts[2].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_VOTE_FAILED); + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there are more yes than no votes", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // act + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true), ccip014pox3.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(ccip014pox3.getVoterInfo(user1Id)); + console.log(ccip014pox3.getMiaVote(mia.cityId, user1Id, false)); + console.log(ccip014pox3.getMiaVote(mia.cityId, user1Id, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user1Id)); + console.log(ccip014pox3.getVoterInfo(user1Id)); + console.log(ccip014pox3.getNycVote(nyc.cityId, user1Id, false)); + console.log(ccip014pox3.getNycVote(nyc.cityId, user1Id, true)); + console.log("\nuser 2 mia:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id)); + console.log(ccip014pox3.getVoterInfo(user2Id)); + console.log(ccip014pox3.getMiaVote(mia.cityId, user2Id, false)); + console.log(ccip014pox3.getMiaVote(mia.cityId, user2Id, true)); + console.log("\nuser 2 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user2Id)); + console.log(ccip014pox3.getVoterInfo(user2Id)); + console.log(ccip014pox3.getNycVote(nyc.cityId, user2Id, false)); + console.log(ccip014pox3.getNycVote(nyc.cityId, user2Id, true)); + */ + + // execute ccip-014 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there are more yes than no votes after a reversal", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // act + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, false), ccip014pox3.voteOnProposal(user2, true)]); + + // switch yes and no vote + const votingBlockReverse = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true), ccip014pox3.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-014 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-014: vote-on-proposal() fails with ERR_USER_NOT_FOUND if user is not registered in ccd003-user-registry", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // act + + // execute yes and no vote + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user3, true)]); + + // assert + //console.log(`votingBlock: ${JSON.stringify(votingBlock, null, 2)}`); + votingBlock.receipts[0].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_USER_NOT_FOUND); + }, +}); + +Clarinet.test({ + name: "ccip-014: vote-on-proposal() fails with ERR_PROPOSAL_NOT_ACTIVE if called after the vote ends", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true), ccip014pox3.voteOnProposal(user2, false)]); + + // execute ccip-014 + passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // act + const votingBlock2 = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true)]); + + // assert + votingBlock2.receipts[0].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_PROPOSAL_NOT_ACTIVE); + }, +}); + +Clarinet.test({ + name: "ccip-014: vote-on-proposal() fails with ERR_VOTED_ALREADY if user already voted with the same value", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true), ccip014pox3.voteOnProposal(user2, false)]); + + // act + const votingBlock2 = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true)]); + + // assert + votingBlock2.receipts[0].result.expectErr().expectUint(CCIP014Pox3.ErrCode.ERR_VOTED_ALREADY); + }, +}); + +Clarinet.test({ + name: "ccip-014: read-only functions return expected values before/after reversal", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox3 = new CCIP014Pox3(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-014 + 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // act + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, false), ccip014pox3.voteOnProposal(user2, true)]); + + // assert + + // overall totals + assertEquals(ccip014pox3.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(ccip014pox3.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(ccip014pox3.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([ccip014pox3.voteOnProposal(user1, true), ccip014pox3.voteOnProposal(user2, false)]); + + // assert + + // overall totals + assertEquals(ccip014pox3.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(ccip014pox3.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(ccip014pox3.getVoterInfo(user2Id).result.expectSome().expectTuple(), { mia: types.uint(219), nyc: types.uint(250), total: types.uint(469), vote: types.bool(false) }); + + // execute ccip-014 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); + + // 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 c59ff307..d4225615 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -91,6 +91,7 @@ export const PROPOSALS = { TEST_CCD007_CITY_STACKING_011: ADDRESS.concat(".test-ccd007-citycoin-stacking-011"), TEST_CCD007_CITY_STACKING_012: ADDRESS.concat(".test-ccd007-citycoin-stacking-012"), TEST_CCD011_STACKING_PAYOUTS_001: ADDRESS.concat(".test-ccd011-stacking-payouts-001"), + TEST_CCIP014_POX3_001: ADDRESS.concat(".test-ccip014-pox-3-001"), }; export const EXTERNAL = { From 835dd3fc353519eb7b64d0ee5362f40f599a7478 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 07:22:04 -0700 Subject: [PATCH 32/47] fix: add tests for mining before/after --- Clarinet.toml | 3 + .../proposals/test-ccip014-pox-3-001.clar | 4 - .../proposals/test-ccip014-pox-3-002.clar | 19 ++++ tests/proposals/ccip014-pox-3.test.ts | 107 +++++++++++++++++- utils/common.ts | 2 + 5 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 tests/contracts/proposals/test-ccip014-pox-3-002.clar diff --git a/Clarinet.toml b/Clarinet.toml index 4f2210c4..e88adbb2 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -410,6 +410,9 @@ path = "tests/contracts/proposals/test-ccd011-stacking-payouts-001.clar" [contracts.test-ccip014-pox-3-001] path = "tests/contracts/proposals/test-ccip014-pox-3-001.clar" +[contracts.test-ccip014-pox-3-002] +path = "tests/contracts/proposals/test-ccip014-pox-3-002.clar" + [repl] costs_version = 2 parser_version = 2 diff --git a/tests/contracts/proposals/test-ccip014-pox-3-001.clar b/tests/contracts/proposals/test-ccip014-pox-3-001.clar index 31c09835..0bc303ad 100644 --- a/tests/contracts/proposals/test-ccip014-pox-3-001.clar +++ b/tests/contracts/proposals/test-ccip014-pox-3-001.clar @@ -34,7 +34,3 @@ (ok true) ) ) - -;; setup each passed proposal here -;; for more fine grained control -;; and ability to use both cities \ No newline at end of file diff --git a/tests/contracts/proposals/test-ccip014-pox-3-002.clar b/tests/contracts/proposals/test-ccip014-pox-3-002.clar new file mode 100644 index 00000000..4bb6f171 --- /dev/null +++ b/tests/contracts/proposals/test-ccip014-pox-3-002.clar @@ -0,0 +1,19 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; Sets up everything required for mining claims after CCIP-014 + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; test-ccd005-city-data-009 + (try! (contract-call? .ccd005-city-data set-coinbase-amounts u1 u10 u100 u1000 u10000 u100000 u1000000 u10000000)) + ;; test-ccd005-city-data-010 + (try! (contract-call? .ccd005-city-data set-coinbase-thresholds u1 u50 u60 u70 u80 u90)) + ;; test-ccd005-city-data-018 + (try! (contract-call? .ccd005-city-data set-coinbase-details u1 u20 u1)) + (ok true) + ) +) diff --git a/tests/proposals/ccip014-pox-3.test.ts b/tests/proposals/ccip014-pox-3.test.ts index d9b187ac..bfc9d65b 100644 --- a/tests/proposals/ccip014-pox-3.test.ts +++ b/tests/proposals/ccip014-pox-3.test.ts @@ -1,5 +1,5 @@ import { Account, Clarinet, Chain, types, assertEquals } from "../../utils/deps.ts"; -import { constructAndPassProposal, mia, nyc, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { CCD006_REWARD_DELAY, 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 { CCIP014Pox3 } from "../../models/proposals/ccip014-pox-3.model.ts"; @@ -507,3 +507,108 @@ Clarinet.test({ block.receipts[2].result.expectOk().expectUint(3); }, }); + +Clarinet.test({ + name: "ccip-014: after upgrade mining disabled, mining-v2 enabled, claims work for both", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip014pox3 = new CCIP014Pox3(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-014 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlockBefore = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + //console.log(`\nminingBlockBefore:\n${JSON.stringify(miningBlockBefore, null, 2)}`); + miningBlockBefore.receipts[0].result.expectOk().expectBool(true); + miningBlockBefore.receipts[1].result.expectOk().expectBool(true); + + // mine in v2 before the upgrade, fails with ERR_INVALID_TREASURY + const miningBlockV2Before = chain.mineBlock([ccd006CityMiningV2.mine(sender, mia.cityName, miningEntries), ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + miningBlockV2Before.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); + miningBlockV2Before.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); + + // 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 4 + // votes are counted in cycles 2-3 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + + // execute single yes vote + const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + // execute ccip-014 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_014); + executeBlock.receipts[2].result.expectOk().expectUint(3); + + /* 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(ccip014pox3.getVoterInfo(userId)); + console.log(ccip014pox3.getMiaVote(mia.cityId, userId, false)); + console.log(ccip014pox3.getMiaVote(mia.cityId, userId, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, userId)); + console.log(ccip014pox3.getVoterInfo(userId)); + console.log(ccip014pox3.getNycVote(nyc.cityId, userId, false)); + console.log(ccip014pox3.getNycVote(nyc.cityId, userId, true)); + console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + */ + + // act + + // mine in v1 after the upgrade, fails with ERR_MINING_DISABLED + const miningBlockAfter = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + miningBlockAfter.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + miningBlockAfter.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + + // mine in v2 after the upgrade + const miningBlockV2After = chain.mineBlock([ccd006CityMiningV2.mine(sender, mia.cityName, miningEntries), ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + //console.log(`\nminingBlockV2After:\n${JSON.stringify(miningBlockV2After, null, 2)}`); + miningBlockV2After.receipts[0].result.expectOk().expectBool(true); + miningBlockV2After.receipts[0].result.expectOk().expectBool(true); + + // fast forward so claims are valid + chain.mineEmptyBlock(CCD006_REWARD_DELAY + 1); + + // pass proposal to set city info for claims + passProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_002); + + const claimBlockHeight = miningBlockBefore.height - 1; + const claimBlockHeightV2 = miningBlockV2After.height - 1; + //console.log(`\nclaim block height: ${claimBlockHeight}`); + //console.log(`\nclaim block height v2: ${claimBlockHeightV2}`); + + // test claim in v1 after upgrade + const miningClaimAfter = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, claimBlockHeight)]); + //console.log(`\nmining claim after:\n${JSON.stringify(miningClaimAfter, null, 2)}`); + miningClaimAfter.receipts[0].result.expectOk().expectBool(true); + + // test claim in v2 after upgrade + const miningClaimV2After = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, mia.cityName, claimBlockHeightV2)]); + //console.log(`\nmining claim v2 after:\n${JSON.stringify(miningClaimV2After, null, 2)}`); + miningClaimV2After.receipts[0].result.expectOk().expectBool(true); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index d4225615..6906c27f 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -6,6 +6,7 @@ import { BaseDao } from "../models/base-dao.model.ts"; // export const START_BLOCK_BASE_DAO = 100; // or 99 export const START_BLOCK_CCD005 = 6; // 6 or 7 export const START_BLOCK_CCD006 = 9; // or 9 or 10 +export const CCD006_REWARD_DELAY = 100; export const ADDRESS = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM"; @@ -92,6 +93,7 @@ export const PROPOSALS = { TEST_CCD007_CITY_STACKING_012: ADDRESS.concat(".test-ccd007-citycoin-stacking-012"), TEST_CCD011_STACKING_PAYOUTS_001: ADDRESS.concat(".test-ccd011-stacking-payouts-001"), TEST_CCIP014_POX3_001: ADDRESS.concat(".test-ccip014-pox-3-001"), + TEST_CCIP014_POX3_002: ADDRESS.concat(".test-ccip014-pox-3-002"), }; export const EXTERNAL = { From d2855e5eafea00650a92ef7c058b33febf152486 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 12:24:25 -0700 Subject: [PATCH 33/47] fix: add missed cycle payouts and related tests --- .../extensions/ccd007-citycoin-stacking.clar | 2 +- contracts/proposals/ccip014-pox-3.clar | 32 ++++- tests/proposals/ccip014-pox-3.test.ts | 124 +++++++++++++----- 3 files changed, 125 insertions(+), 33 deletions(-) diff --git a/contracts/extensions/ccd007-citycoin-stacking.clar b/contracts/extensions/ccd007-citycoin-stacking.clar index c225106f..16a69d97 100644 --- a/contracts/extensions/ccd007-citycoin-stacking.clar +++ b/contracts/extensions/ccd007-citycoin-stacking.clar @@ -240,7 +240,7 @@ (stacker (get-stacker cityId cycle userId)) (userStacked (get stacked stacker)) ) - (if (and (or (not (var-get stackingEnabled)) (< cycle (get-reward-cycle burn-block-height))) (> userStacked u0)) + (if (and (or (not (var-get stackingEnabled)) (< cycle (get-reward-cycle burn-block-height))) (> userStacked u0)) (some (/ (* (unwrap! (get reward cycleStats) (some u0)) userStacked) (get total cycleStats))) none ) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 9e71cc8c..4424824e 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -16,6 +16,8 @@ ;; CONSTANTS +(define-constant SELF (as-contract tx-sender)) +(define-constant MISSED_PAYOUT u1) (define-constant CCIP_014 { name: "Upgrade to pox-3", link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", @@ -93,8 +95,8 @@ (try! (contract-call? .ccd002-treasury-nyc-mining withdraw-stx nycBalance .ccd002-treasury-nyc-mining-v2)) ;; delegate stack the STX in the mining treasuries (up to 50M STX each) - ;; MAINNET: TODO - ;; MAINNET: TODO + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox-fast-pool-v2 + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox-fast-pool-v2 (try! (contract-call? .ccd002-treasury-mia-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) (try! (contract-call? .ccd002-treasury-nyc-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) @@ -105,6 +107,27 @@ ;; disable original mining contract (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) + ;; set pool operator to self + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator SELF)) + + ;; pay out missed MIA cycles 56, 57, 58, 59 with 1 uSTX each + ;; MAINNET: u56, u57, u58, u59 + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u1 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u2 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u3 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u4 MISSED_PAYOUT))) + + ;; pay out missed NYC cycles 56, 57, 58, 59 with 1 uSTX each + ;; MAINNET: u56, u57, u58, u59 + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u1 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u2 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u3 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u4 MISSED_PAYOUT))) + + ;; set pool operator to FAST pool + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox-fast-pool-v2 + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + (ok true) ) ) @@ -308,3 +331,8 @@ (define-private (scale-down (a uint)) (/ a VOTE_SCALE_FACTOR) ) + +;; INITIALIZATION + +;; fund proposal with 8 uSTX for payouts from deployer +(stx-transfer? (* MISSED_PAYOUT u8) tx-sender (as-contract tx-sender)) diff --git a/tests/proposals/ccip014-pox-3.test.ts b/tests/proposals/ccip014-pox-3.test.ts index bfc9d65b..0baf635f 100644 --- a/tests/proposals/ccip014-pox-3.test.ts +++ b/tests/proposals/ccip014-pox-3.test.ts @@ -60,10 +60,11 @@ Clarinet.test({ stackingBlock.receipts[0].result.expectOk().expectBool(true); stackingBlock.receipts[1].result.expectOk().expectBool(true); - // progress the chain to cycle 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); // act @@ -109,6 +110,7 @@ Clarinet.test({ // progress the chain to avoid underflow in // stacking reward cycle calculation chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for CCIP-014 const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); @@ -119,10 +121,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); // act @@ -185,10 +188,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); // act @@ -263,10 +267,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); // act @@ -320,10 +325,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); // act @@ -364,10 +370,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // 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 @@ -412,10 +419,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // 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 @@ -461,10 +469,11 @@ Clarinet.test({ 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 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); // act @@ -509,11 +518,12 @@ Clarinet.test({ }); Clarinet.test({ - name: "ccip-014: after upgrade mining disabled, mining-v2 enabled, claims work for both", + name: "ccip-014: after upgrade mining disabled, mining-v2 enabled, mining and stacking claims work as expected", fn(chain: Chain, accounts: Map<string, Account>) { // 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 ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); @@ -521,11 +531,12 @@ Clarinet.test({ const miningEntries = [25000000, 25000000]; const amountStacked = 500; - const lockPeriod = 10; + const lockPeriod = 4; // progress the chain to avoid underflow in // stacking reward cycle calculation chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + // prepare for CCIP-014 const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); @@ -541,13 +552,37 @@ Clarinet.test({ miningBlockV2Before.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); // 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)]); + const stackingBlock = chain.mineBlock([ccd007CityStacking.stack(user1, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user1, nyc.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, mia.cityName, amountStacked, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod)]); stackingBlock.receipts[0].result.expectOk().expectBool(true); + stackingBlock.receipts[1].result.expectOk().expectBool(true); + stackingBlock.receipts[2].result.expectOk().expectBool(true); + stackingBlock.receipts[3].result.expectOk().expectBool(true); - // progress the chain to cycle 4 + // progress the chain to cycle 5 // votes are counted in cycles 2-3 - chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 5 + 10); - ccd007CityStacking.getCurrentRewardCycle().result.expectUint(4); + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // is-cycle-paid returns false for cycles 1-4 for MIA + ccd007CityStacking.isCyclePaid(mia.cityId, 1).result.expectBool(false); + ccd007CityStacking.isCyclePaid(mia.cityId, 2).result.expectBool(false); + ccd007CityStacking.isCyclePaid(mia.cityId, 3).result.expectBool(false); + ccd007CityStacking.isCyclePaid(mia.cityId, 4).result.expectBool(false); + + // is-cycle-paid returns false for cycles 1-4 for NYC + ccd007CityStacking.isCyclePaid(nyc.cityId, 1).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 2).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 3).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 4).result.expectBool(false); + + // claim-stacking-reward before, fails with ERR_NOTHING_TO_CLAIM + // since cycle is not paid out yet + const claimStackingRewardBefore = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, mia.cityName, 2), ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 2), ccd007CityStacking.claimStackingReward(user2, mia.cityName, 2), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 2)]); + claimStackingRewardBefore.receipts[0].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardBefore.receipts[1].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardBefore.receipts[2].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardBefore.receipts[3].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); // execute single yes vote const votingBlock = chain.mineBlock([ccip014pox3.voteOnProposal(user1, true)]); @@ -610,5 +645,34 @@ Clarinet.test({ const miningClaimV2After = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, mia.cityName, claimBlockHeightV2)]); //console.log(`\nmining claim v2 after:\n${JSON.stringify(miningClaimV2After, null, 2)}`); miningClaimV2After.receipts[0].result.expectOk().expectBool(true); + + // is-cycle-paid returns true for cycles 1-4 for MIA + ccd007CityStacking.isCyclePaid(mia.cityId, 1).result.expectBool(true); + ccd007CityStacking.isCyclePaid(mia.cityId, 2).result.expectBool(true); + ccd007CityStacking.isCyclePaid(mia.cityId, 3).result.expectBool(true); + ccd007CityStacking.isCyclePaid(mia.cityId, 4).result.expectBool(true); + + // is-cycle-paid returns true for cycles 1-4 for NYC + ccd007CityStacking.isCyclePaid(nyc.cityId, 1).result.expectBool(true); + ccd007CityStacking.isCyclePaid(nyc.cityId, 2).result.expectBool(true); + ccd007CityStacking.isCyclePaid(nyc.cityId, 3).result.expectBool(true); + ccd007CityStacking.isCyclePaid(nyc.cityId, 4).result.expectBool(true); + + // claim-stacking-reward after for cycle 2 + // fails with ERR_NOTHING_TO_CLAIM since 1 uSTX cannot be divided among participants + const claimStackingRewardAfter = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, mia.cityName, 2), ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 2), ccd007CityStacking.claimStackingReward(user2, mia.cityName, 2), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 2)]); + //console.log(`\nclaim stacking reward cycle 2 after:\n${JSON.stringify(claimStackingRewardAfter, null, 2)}`); + claimStackingRewardAfter.receipts[0].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardAfter.receipts[1].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardAfter.receipts[2].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardAfter.receipts[3].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + + // claim-stacking-reward after for cycle 4 + const claimStackingRewardAfter2 = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, mia.cityName, 4), ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 4), ccd007CityStacking.claimStackingReward(user2, mia.cityName, 4), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 4)]); + //console.log(`\nclaim stacking reward cycle 4 after:\n${JSON.stringify(claimStackingRewardAfter2, null, 2)}`); + claimStackingRewardAfter2.receipts[0].result.expectOk().expectBool(true); + claimStackingRewardAfter2.receipts[1].result.expectOk().expectBool(true); + claimStackingRewardAfter2.receipts[2].result.expectOk().expectBool(true); + claimStackingRewardAfter2.receipts[3].result.expectOk().expectBool(true); }, }); From 66786a2a4e11bdd68953ec0479ed7310711ed03d Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 13:11:02 -0700 Subject: [PATCH 34/47] fix: update pool operator for payouts --- contracts/proposals/ccip014-pox-3.clar | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 4424824e..c4ba8ff0 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -124,8 +124,8 @@ (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u3 MISSED_PAYOUT))) (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u4 MISSED_PAYOUT))) - ;; set pool operator to FAST pool - ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox-fast-pool-v2 + ;; set pool operator to Friedger pool + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP (try! (contract-call? .ccd011-stacking-payouts set-pool-operator 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) (ok true) From 7f7eccd6e6f1814f9fccef5a7bc40484d313ffef Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 13:37:15 -0700 Subject: [PATCH 35/47] fix: skip mining-v2 check since not enabled yet --- tests/base-dao.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/base-dao.test.ts b/tests/base-dao.test.ts index 9201dc1e..38436934 100644 --- a/tests/base-dao.test.ts +++ b/tests/base-dao.test.ts @@ -203,6 +203,7 @@ Clarinet.test({ // assert for (const ext of Object.values(EXTENSIONS)) { if (ext === EXTENSIONS.CCD008_CITY_ACTIVATION) continue; // temporarily skip + if (ext === EXTENSIONS.CCD006_CITYCOIN_MINING_V2) continue; // skip, not enabled until CCIP-014 baseDao.isExtension(ext).result.expectBool(true); } }, From 188d41c8965b4fcd3c9e1d76d5d3d1f40ec6890a Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 13:37:46 -0700 Subject: [PATCH 36/47] fix: remove v2 mining tests in favor of ccip-014 testing Also updates a balance check in original mining tests due to CCIP-014 deployment using 8 uSTX --- .../ccd006-citycoin-mining-v2.test.ts | 1632 ----------------- .../extensions/ccd006-citycoin-mining.test.ts | 37 +- 2 files changed, 20 insertions(+), 1649 deletions(-) delete mode 100644 tests/extensions/ccd006-citycoin-mining-v2.test.ts diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts deleted file mode 100644 index f1b9bed3..00000000 --- a/tests/extensions/ccd006-citycoin-mining-v2.test.ts +++ /dev/null @@ -1,1632 +0,0 @@ -/** - * Test class is structured; - * 0. AUTHORIZATION CHECKS - * 1. mine - * 2. claim-mining-reward - * 3. reward-delay - * 4. mining status - * 5. read-only functions - */ -import { Account, assert, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; -import { constructAndPassProposal, EXTENSIONS, mia, passProposal, PROPOSALS } from "../../utils/common.ts"; -import { CCD002Treasury } from "../../models/extensions/ccd002-treasury.model.ts"; -import { CCD003UserRegistry } from "../../models/extensions/ccd003-user-registry.model.ts"; -import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.ts"; -import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; -import { CCD010CoreV2Adapter } from "../../models/extensions/ccd010-core-v2-adapter.model.ts"; -import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; -import { CCIP014 } from "../../models/proposals/ccip014-pox-3.model.ts"; - -// ============================= -// INTERNAL DATA / FUNCTIONS -// ============================= - -const rewardDelay = 100; - -// reusable mining functions - -const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, userId: number, miningStatsAt: any, minerAt: any) => { - let expectedStats: any = { - amount: types.uint(miningStatsAt.amount), - claimed: types.bool(miningStatsAt.claimed), - miners: types.uint(miningStatsAt.miners), - }; - assertEquals(ccd006CityMining.getMiningStats(cityId, height).result.expectTuple(), expectedStats); - - expectedStats = { - commit: types.uint(minerAt.commit), - high: types.uint(minerAt.high), - low: types.uint(minerAt.low), - winner: types.bool(minerAt.winner), - }; - assertEquals(ccd006CityMining.getMiner(cityId, height, userId).result.expectTuple(), expectedStats); -}; - -const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006CityMining, chain: Chain, sender: Account): any => { - const entries: number[] = [10]; - const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); - const claimHeight = miningBlock.height - 1; - chain.mineEmptyBlock(rewardDelay + 1); - const miningClaimBlock = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight), ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); - - miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); - miningBlock.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); - let winner = 0; - let coinbase = 0; - if (miningClaimBlock.receipts[0].result === "(ok true)") { - //console.log("======== USER 1 WINS =========================") - ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(1); - coinbase = Number(ccd006CityMining.getCoinbaseAmount(mia.cityId, claimHeight).result.substring(1)); - miningClaimBlock.receipts[0].result.expectOk().expectBool(true); - /** - console.log("getCoinbaseAmount : " + coinbase) - console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(mia.cityId, user1.address, claimHeight).result.expectSome().expectTuple()) - console.log("getMiningStats : ", ccd006CityMining.getMiningStats(mia.cityId, claimHeight)) - */ - winner = 1; - } else if (miningClaimBlock.receipts[1].result === "(ok true)") { - //console.log("======== USER 2 WINS =========================") - ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(2); - //miningClaimBlock.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); - miningClaimBlock.receipts[1].result.expectOk().expectBool(true); - coinbase = Number(ccd006CityMining.getCoinbaseAmount(mia.cityId, claimHeight).result.substring(1)); - winner = 2; - /** - console.log("getCoinbaseAmount : " + coinbase) - console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(mia.cityId, user2.address, claimHeight).result.expectSome().expectTuple()) - console.log("getMiningStats : ", ccd006CityMining.getMiningStats(mia.cityId, claimHeight)) - */ - } else { - console.log("======== NOONE WINS ========================="); - return 3; - } - return { miningBlock, miningClaimBlock, claimHeight, winner, coinbase }; -}; - -// ============================= -// 0. AUTHORIZATION CHECKS -// ============================= - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: is-dao-or-extension() fails when called directly", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - - // assert - ccd006CityMining.isDaoOrExtension().result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); - }, -}); - -// Extension callback - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: callback() succeeds when called directly", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const { receipts } = chain.mineBlock([ccd006CityMining.callback(sender, "test")]); - - // assert - assertEquals(receipts.length, 1); - receipts[0].result.expectOk().expectBool(true); - }, -}); - -// ============================= -// 1. mine -// ============================= - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if city is not registered", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [10, 10]; - const { receipts } = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - assertEquals(receipts.length, 1); - receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_CITY); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if city is not active", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [10, 10]; - // create city ids - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - // set city details (fails before this check) - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - // set city treasury (fails before this check) - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_012); - // generalize to three functions: - // passCCIP014 which will vote and execute - // voteCCIP014 which votes for given user - // execCCIP014 which executes the proposal - console.log(passProposal(chain, accounts, PROPOSALS.CCIP_014)); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INACTIVE_CITY); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if city details are not set", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [10, 10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - //passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_ACTIVATION_DETAILS); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if city treasury is not set", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [10, 10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if mining contract is not a valid dao extension", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [10, 10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_001); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); - ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD003UserRegistry.ErrCode.ERR_UNAUTHORIZED); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if user has insufficient balance", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [100000000000001]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NOT_ENOUGH_FUNDS); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() succeeds if user's cumulative commit uses their exact balance", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [50000000000000, 50000000000000]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - passProposal(chain, accounts, PROPOSALS.CCIP_014); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - ccd002Treasury.getBalanceStx().result.expectUint(100000000000000); - block.receipts[0].result.expectOk().expectBool(true); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if user's cumulative commit leaves insufficient balance", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [50000000000000, 50000000000000, 1]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - ccd002Treasury.getBalanceStx().result.expectUint(0); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NOT_ENOUGH_FUNDS); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if city is inactive", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries = [10, 10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_003); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); - ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INACTIVE_CITY); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if called with no commit amounts", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = []; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - ccd005CityData.getCityTreasuryId(mia.cityId, mia.treasuryV1Name).result.expectSome().expectUint(1); - ccd005CityData.getCityTreasuryName(mia.cityId, mia.treasuryV1Id).result.expectSome().expectAscii(mia.treasuryV1Name); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_COMMITS); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if a commit amount in the list is zero", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10, 10, 10, 0, 10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_COMMITS); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() succeeds and mines 1 block for 1 user", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); - - // assert - const firstBlock = block.height - 1; - const lastBlock = firstBlock; - const totalAmount = 10; - const totalBlocks = 1; - const userId = 1; - - block.receipts[0].result.expectOk().expectBool(true); - - block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${mia.treasuryV1Contract}`); - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; - //console.log(block.receipts[0].events[1].contract_event.value) - //ccd006CityMining.isBlockWinner(mia.cityId, user.address, firstBlock).result.expectBool(true) - const expectedStats2 = { - commit: types.uint(10), - high: types.uint(10), - low: types.uint(0), - winner: types.bool(false), - }; - assertEquals(ccd006CityMining.getMiner(mia.cityId, firstBlock, userId).result.expectTuple(), expectedStats2); - const expectedStats = { - amount: types.uint(10), - claimed: types.bool(false), - miners: types.uint(1), - }; - assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedStats); - ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); - - block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - block.receipts[0].result.expectOk().expectBool(true); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() succeeds and mines 200 blocks for 1 user", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const userId = 1; - const commitAmount = 10; - const numberOfBlocks = 200; - const entries = new Array<number>(numberOfBlocks).fill(commitAmount); - const totalAmount = entries.reduce((a, b) => a + b, 0); - const totalBlocks = entries.length; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); - - const firstBlock = block.height - 1; - const lastBlock = firstBlock + entries.length - 1; - - // assert - - block.receipts[0].result.expectOk().expectBool(true); - block.receipts[0].events.expectSTXTransferEvent(totalAmount, user.address, `${sender.address}.${mia.treasuryV1Contract}`); - - // mining stats at block - const expectedStats = { - amount: types.uint(commitAmount), - claimed: types.bool(false), - miners: types.uint(1), - }; - // miner stats at each block - const expectedMinerStats = { - commit: types.uint(commitAmount), - high: types.uint(commitAmount), - low: types.uint(0), - winner: types.bool(false), - }; - for (let i = 0; i < entries.length; i++) { - assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedStats); - assertEquals(ccd006CityMining.getMiner(mia.cityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); - } - - ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; - block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - block.receipts[0].result.expectOk().expectBool(true); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() successfully mines 100 blocks for 3 users", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!]; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const userIds = [1, 2, 3]; - const commitAmount = 100; - const numberOfBlocks = 100; - const entries = new Array<number>(numberOfBlocks).fill(commitAmount); - const totalCommit = entries.reduce((a, b) => a + b, 0); - const totalBlocks = entries.length; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - const block = chain.mineBlock([ccd006CityMining.mine(users[0], mia.cityName, entries), ccd006CityMining.mine(users[1], mia.cityName, entries), ccd006CityMining.mine(users[2], mia.cityName, entries)]); - const firstBlock = block.height - 1; - const lastBlock = firstBlock + entries.length - 1; - - // assert - - for (let i = 0; i < userIds.length; i++) { - // check that each event succeeded - block.receipts[i].result.expectOk().expectBool(true); - // check that each event transferred the correct amount to the correct address - block.receipts[i].events.expectSTXTransferEvent(totalCommit, users[i].address, `${sender.address}.${mia.treasuryV1Contract}`); - } - - // mining stats at block - const expectedStats = { - amount: types.uint(commitAmount * users.length), - claimed: types.bool(false), - miners: types.uint(users.length), - }; - // loop through each block to check miner stats - for (let i = 0; i < entries.length; i++) { - assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedStats); - // loop through each user - for (let j = 0; j < userIds.length; j++) { - // check the data - const lastCommit = commitAmount * j; - const currentCommit = commitAmount * (j + 1); - const expectedMinerStats = { - commit: types.uint(commitAmount), - high: types.uint(currentCommit), - low: types.uint(j === 0 ? 0 : lastCommit + 1), - winner: types.bool(false), - }; - assertEquals(ccd006CityMining.getMiner(mia.cityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); - } - } - - // check the print message for each user - for (let i = 0; i < userIds.length; i++) { - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalCommit)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userIds[i])}}`; - block.receipts[i].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - block.receipts[i].result.expectOk().expectBool(true); - } - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if user has already mined", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const totalAmount = 30; - const totalBlocks = 3; - const userId = 1; - const entries: number[] = [10, 10, 10]; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_V2_002); - const block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries), ccd006CityMining.mine(sender, mia.cityName, entries)]); - const firstBlock = block.height - 1; - const lastBlock = firstBlock + entries.length - 1; - - // assert - - block.receipts[0].result.expectOk().expectBool(true); - block.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_MINED); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - block.receipts[0].events.expectSTXTransferEvent(totalAmount, sender.address, `${sender.address}.${mia.treasuryV1Contract}`); - - // block.receipts[1].events.expectSTXTransferEvent(totalAmount, sender.address, `${sender.address}.${mia.treasuryV1Contract}`); - - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; - - block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - // Expecting ERR_ALREADY_MINED if same user tried mine twice at same height ? - block.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_MINED); - // and for the treasury balance to show the first tx only .. - ccd002Treasury.getBalanceStx().result.expectUint(totalAmount); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() keeps track of mining stats for 4 users mining 3 blocks concurrently", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!, accounts.get("wallet_4")!]; - - const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const ccd003UserRegistry = new CCD003UserRegistry(chain, sender, "ccd003-user-registry"); - - const totalAmount = 120; - const totalBlocks = 3; - const entries: number[] = [10, 10, 10]; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - const block = chain.mineBlock([ccd006CityMining.mine(users[0], mia.cityName, entries), ccd006CityMining.mine(users[1], mia.cityName, entries), ccd006CityMining.mine(users[2], mia.cityName, entries), ccd006CityMining.mine(users[3], mia.cityName, entries)]); - const firstBlock = block.height - 1; - const lastBlock = firstBlock + entries.length - 1; - - // assert - let miningStatsAt, minerAt; - for (let idx = 0; idx < 4; idx++) { - block.receipts[idx].result.expectOk().expectBool(true); - ccd003UserRegistry - .getUserId(users[idx].address) - .result.expectSome() - .expectUint(idx + 1); - ccd003UserRegistry - .getUser(idx + 1) - .result.expectSome() - .expectPrincipal(users[idx].address); - block.receipts[idx].events.expectSTXTransferEvent(30, users[idx].address, `${sender.address}.${mia.treasuryV1Contract}`); - - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount / 4)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(idx + 1)}}`; - block.receipts[idx].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - } - - for (let idx1 = 0; idx1 < entries.length; idx1++) { - // 3 blocks - for (let idx = 0; idx < users.length; idx++) { - // 4 users - miningStatsAt = { amount: users.length * entries[idx1], claimed: false, miners: users.length }; - minerAt = { commit: 10, high: 10 * (idx + 1), low: idx === 0 ? 0 : idx * 10 + 1, winner: false }; - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock + idx1), (idx + 1), miningStatsAt, minerAt); - checkMiningData(ccd006CityMining, mia.cityId, firstBlock + idx1, idx + 1, miningStatsAt, minerAt); - } - } - - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - // check that total balance was transferred to treasury - ccd002Treasury.getBalanceStx().result.expectUint(totalAmount); - ccd006CityMining.getHighValue(mia.cityId, firstBlock).result.expectUint(totalAmount / 3); - ccd006CityMining.getHighValue(mia.cityId, lastBlock).result.expectUint(totalAmount / 3); - ccd006CityMining.getHighValue(mia.cityId, lastBlock + 1).result.expectUint(0); - }, -}); - -// ============================= -// 2. claim-mining-reward -// ============================= - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: is-block-winner() correctly identifies winning miner who has not claimed", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); - const totalAmount = 10; - const totalBlocks = 1; - const entries: number[] = [10]; - gt.getBalance(user1.address).result.expectOk().expectUint(0); - gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); - // console.log(`miningBlock:\n${JSON.stringify(miningBlock, null, 2)}}`); - const claimHeight = miningBlock.height - 1; - const lastBlock = claimHeight + totalBlocks - 1; - chain.mineEmptyBlock(rewardDelay + 1); - - // assert - miningBlock.receipts[0].result.expectOk().expectBool(true); - - // Check stx transfer events - miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); - - // Check mining events - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; - miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - gt.getBalance(user1.address).result.expectOk().expectUint(0); - gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); - const expected = { - claimed: types.bool(false), - winner: types.bool(true), - }; - const isBlockWinner = ccd006CityMining.isBlockWinner(mia.cityId, user1.address, claimHeight); - //console.log(JSON.stringify(isBlockWinner, null, 2)); - // console.log(`isBlockWinner:\n${JSON.stringify(isBlockWinner, null, 2)}}`); - assertEquals(isBlockWinner.result.expectSome().expectTuple(), expected); - // is-block-winner calculates the winning status of given user. - // get-block-winner reads it from the map which is written by claim-mining-reward. - // so user1 is not returned by the following. This is correct, since the map isn't written - // to until the reward is claimed, - // we would expect the value to be none here while the output of isBlockWinner() above - // will be (some (claimed false) (winner true)). - ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectNone(); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: is-block-winner() correctly identifies winning miner who has claimed", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); - const totalAmount = 10; - const totalBlocks = 1; - const entries: number[] = [10]; - gt.getBalance(user1.address).result.expectOk().expectUint(0); - gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); - const miningHeight = miningBlock.height - 1; - chain.mineEmptyBlock(rewardDelay + 1); - const miningClaimBlock = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, miningHeight)]); - - // assert - miningBlock.receipts[0].result.expectOk().expectBool(true); - // Check mining event - let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(miningHeight)}, lastBlock: ${types.uint(miningHeight)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; - miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - // Check mining claim event - expectedPrintMsg = `{cityId: u1, cityName: "mia", claimHeight: ${types.uint(miningHeight)}, event: "mining-claim", userId: ${types.uint(1)}}`; - miningClaimBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - // Check stx transfer events - miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); - // check mia token balances - gt.getBalance(user1.address).result.expectOk().expectUint(10); - gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); - const expected = { - claimed: types.bool(true), - winner: types.bool(true), - }; - assertEquals(ccd006CityMining.isBlockWinner(mia.cityId, user1.address, miningHeight).result.expectSome().expectTuple(), expected); - ccd006CityMining.getBlockWinner(mia.cityId, miningHeight).result.expectSome().expectUint(1); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible for an unknown city", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const { receipts } = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, 50)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(100); - receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_CITY); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if current tip height is less than maturity height ", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - const block = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, 50)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(100); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_REWARD_IMMATURE); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if current tip height is equal to maturity height", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - const claimBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - chain.mineEmptyBlock(rewardDelay - 1); - const block = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, claimBlock.height)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_REWARD_IMMATURE); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if user is not registered", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - const claimHeight = 5; // one less than actual bh - chain.mineEmptyBlock(rewardDelay); - const block = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, claimHeight)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_USER); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() returns ERR_NO_MINER_DATA if user did not mine in that block", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - const claimHeight = 6; // one less than actual bh - chain.mineEmptyBlock(rewardDelay); - const block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if a user tries claiming another users rewards", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - let block = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, entries)]); - const claimHeight = block.height - 1; // one less than actual bh - chain.mineEmptyBlock(rewardDelay); - block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_USER); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if there is nothing to mint at the given claim height", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); - const claimHeight = block.height - 1; // one less than actual bh - chain.mineEmptyBlock(rewardDelay); - block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails with ERR_NOTHING_TO_MINT if the coinbase amounts have not been set for the given city", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); - block.receipts[0].result.expectOk().expectBool(true); - block.receipts[0].result.expectOk().expectBool(true); - const firstBlock = block.height - 1; - const lastBlock = firstBlock; - const totalAmount = 10; - const totalBlocks = 1; - const userId = 1; - block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${mia.treasuryV1Contract}`); - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; - block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; - //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; - // dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); - - const claimHeight = block.height - 1; - chain.mineEmptyBlock(rewardDelay + 1); - block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); - - // assert - - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails with ERR_NOTHING_TO_MINT if called at the wrong claim height", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_007); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - let block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); - block.receipts[0].result.expectOk().expectBool(true); - block.receipts[0].result.expectOk().expectBool(true); - const firstBlock = block.height - 1; - const lastBlock = firstBlock; - const totalAmount = 10; - const totalBlocks = 1; - const userId = 1; - block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${mia.treasuryV1Contract}`); - const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; - block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; - //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); - - const claimHeight = block.height - 1; - chain.mineEmptyBlock(rewardDelay + 1); - - block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); - - // assert - - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if user is not the winner or if there is nothing to mint", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const user2 = accounts.get("wallet_2")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const totalAmount = 10; - const totalBlocks = 1; - const entries: number[] = [10]; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_007); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - const block1 = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); - const firstBlock = block1.height - 1; - const lastBlock = firstBlock + entries.length - 1; - - chain.mineEmptyBlock(rewardDelay); - const block2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, firstBlock), ccd006CityMining.claimMiningReward(user2, mia.cityName, firstBlock)]); - - // assert - - block1.receipts[0].result.expectOk().expectBool(true); - block1.receipts[1].result.expectOk().expectBool(true); - - if (block2.receipts[0].result === "(err u6011)") { - //console.log("USER 2 WINS"); - block2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); - block2.receipts[1].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); - } else { - //console.log("USER 1 WINS"); - block2.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); - block2.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); - } - - block1.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); - block1.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); - - let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; - block1.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; - block1.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - - //ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectUint(2); - ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectNone(); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() user makes successful claim", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const user2 = accounts.get("wallet_2")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const totalAmount = 10; - const totalBlocks = 1; - const entries = [10]; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); - // console.log(JSON.stringify(miningBlock, null, 2)); - const claimHeight = miningBlock.height - 1; - const lastBlock = claimHeight + totalBlocks - 1; - chain.mineEmptyBlock(rewardDelay + 1); - - const miningClaimUser1 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); - const miningClaimUser2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); - - // assert - - const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(mia.cityId).result.expectTuple(); - // verify coinbase amounts - const expectedAmounts = { - cbaBonus: types.uint(10), - cba1: types.uint(100), - cba2: types.uint(1000), - cba3: types.uint(10000), - cba4: types.uint(100000), - cba5: types.uint(1000000), - cbaDefault: types.uint(10000000), - }; - assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); - // verify coinbase thresholds - const expectedThresholds = { - cbt1: types.uint(50), - cbt2: types.uint(60), - cbt3: types.uint(70), - cbt4: types.uint(80), - cbt5: types.uint(90), - }; - assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); - - miningBlock.receipts[0].result.expectOk().expectBool(true); - miningBlock.receipts[0].events.expectSTXTransferEvent(totalAmount, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); - miningBlock.receipts[1].result.expectOk().expectBool(true); - miningBlock.receipts[1].events.expectSTXTransferEvent(totalAmount, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); - - //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; - //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (2), miningStatsAt, minerAt); - - //console.log(`miningClaimUser1:\n${JSON.stringify(miningClaimUser1, null, 2)}\n`); - //console.log(`miningClaimUser2:\n${JSON.stringify(miningClaimUser2, null, 2)}\n`); - - let winner: number; - - if (miningClaimUser2.receipts[0].result === "(err u6014)") { - //console.log("USER 1 WINS"); - miningClaimUser1.receipts[0].result.expectOk().expectBool(true); - miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); - winner = 1; - } else { - //console.log("USER 2 WINS"); - miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); - miningClaimUser2.receipts[0].result.expectOk().expectBool(true); - winner = 2; - } - - // {cityId: u1, cityName: \"mia\", cityTreasury: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining, event: \"mining\", firstBlock: u10, lastBlock: u10, totalAmount: u10, totalBlocks: u1, userId: u1}"} - let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; - // console.log(JSON.stringify(miningBlock.receipts[0].events), null, 2); - miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; - miningBlock.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - - //ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectUint(2); - ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(winner); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() two miners compete and each wins within 10% of half the time", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const user2 = accounts.get("wallet_2")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); - gt.getBalance(user1.address).result.expectOk().expectUint(0); - gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - // add mining treasury - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - // assert - let result; - let winner1 = 0; - let winner2 = 0; - let count1 = 0; - let count2 = 0; - const runs = 100; - for (let i = 0; i < runs; i++) { - result = twoMinersMine(user1, user2, ccd006CityMining, chain, sender); - if (result.winner === 1) { - count1 = count1 + result.coinbase; - winner1++; - } else if (result.winner === 2) { - count2 = count2 + result.coinbase; - winner2++; - } - } - gt.getBalance(user1.address).result.expectOk().expectUint(count1); - gt.getBalance(user2.address).result.expectOk().expectUint(count2); - // ensure that each wins within 10% of half the time - assert(winner1 > runs / 2 - (runs * 10) / 100); - assert(winner2 > runs / 2 - (runs * 10) / 100); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if user claims at incorrect height", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - // add mining treasury - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - // assert - const entries = [10]; - const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries)]); - const claimHeight = miningBlock.height - 1; - chain.mineEmptyBlock(rewardDelay + 1); - const miningClaimBlock = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight + 1), ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight - 1), ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); - - //console.log(`miningClaimBlock: ${JSON.stringify(miningClaimBlock, null, 2)}}`); - - // assert - miningClaimBlock.receipts[2].result.expectOk().expectBool(true); - miningClaimBlock.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); - miningClaimBlock.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); - }, -}); - -// ============================= -// 3. REWARD DELAY -// ============================= - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: set-reward-delay() can only be called by the dao", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const { receipts } = chain.mineBlock([ccd006CityMining.setRewardDelay(sender, 50)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(100); - receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: set-reward-delay() cannot set a zero block delay", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_004); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(100); - receipts[3].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_DELAY); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: set-reward-delay() successfully changes the reward delay when called by the dao", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - ccd006CityMining.getRewardDelay().result.expectUint(100); - - // act - const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_003); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(50); - assertEquals(receipts.length, 4); - receipts[3].result.expectOk(); - }, -}); - -// ============================= -// 4. MINING STATUS -// ============================= - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: mine() fails if mining is disabled in the contract", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const entries: number[] = [10]; - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - const block = chain.mineBlock([ccd006CityMining.mine(user, mia.cityName, entries)]); - - // assert - const firstBlock = block.height - 1; - const userId = 1; - - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); - - const expectedMiningStats = { - amount: types.uint(0), - claimed: types.bool(false), - miners: types.uint(0), - }; - assertEquals(ccd006CityMining.getMiningStats(mia.cityId, firstBlock).result.expectTuple(), expectedMiningStats); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() returns ERR_NO_MINER_DATA after mining is disabled if user did not mine in that block", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user = accounts.get("wallet_1")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); - const claimHeight = 6; // one less than actual bh - chain.mineEmptyBlock(rewardDelay); - const block = chain.mineBlock([ccd006CityMining.claimMiningReward(user, mia.cityName, claimHeight)]); - - // assert - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: claim-mining-reward() succeeds after mining is disabled", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const user1 = accounts.get("wallet_1")!; - const user2 = accounts.get("wallet_2")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - const entries = [10]; - const totalAmount = entries.reduce((a, b) => a + b, 0); - const totalBlocks = entries.length; - - // act - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_002); - ccd005CityData.getCityTreasuryNonce(mia.cityId).result.expectUint(1); - - const miningBlock = chain.mineBlock([ccd006CityMining.mine(user1, mia.cityName, entries), ccd006CityMining.mine(user2, mia.cityName, entries)]); - // console.log(JSON.stringify(miningBlock, null, 2)); - const claimHeight = miningBlock.height - 1; - const lastBlock = claimHeight + totalBlocks - 1; - chain.mineEmptyBlock(rewardDelay + 1); - - // disable mining - passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); - - const miningClaimUser1 = chain.mineBlock([ccd006CityMining.claimMiningReward(user1, mia.cityName, claimHeight)]); - const miningClaimUser2 = chain.mineBlock([ccd006CityMining.claimMiningReward(user2, mia.cityName, claimHeight)]); - - // assert - - const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(mia.cityId).result.expectTuple(); - // verify coinbase amounts - const expectedAmounts = { - cbaBonus: types.uint(10), - cba1: types.uint(100), - cba2: types.uint(1000), - cba3: types.uint(10000), - cba4: types.uint(100000), - cba5: types.uint(1000000), - cbaDefault: types.uint(10000000), - }; - assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); - // verify coinbase thresholds - const expectedThresholds = { - cbt1: types.uint(50), - cbt2: types.uint(60), - cbt3: types.uint(70), - cbt4: types.uint(80), - cbt5: types.uint(90), - }; - assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); - - miningBlock.receipts[0].result.expectOk().expectBool(true); - miningBlock.receipts[1].result.expectOk().expectBool(true); - - let winner: number; - - if (miningClaimUser2.receipts[0].result === "(err u6014)") { - //console.log("USER 1 WINS"); - miningClaimUser1.receipts[0].result.expectOk().expectBool(true); - miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); - winner = 1; - } else { - //console.log("USER 2 WINS"); - miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); - miningClaimUser2.receipts[0].result.expectOk().expectBool(true); - winner = 2; - } - - miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${mia.treasuryV1Contract}`); - miningBlock.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${mia.treasuryV1Contract}`); - // {cityId: u1, cityName: \"mia\", cityTreasury: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining, event: \"mining\", firstBlock: u10, lastBlock: u10, totalAmount: u10, totalBlocks: u1, userId: u1}"} - let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(1)}}`; - // console.log(JSON.stringify(miningBlock.receipts[0].events), null, 2); - miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${mia.treasuryV1Contract}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; - miningBlock.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); - - //dumpMiningData(ccd006CityMining, mia.cityId, (firstBlock), (1), miningStatsAt, minerAt); - ccd006CityMining.getRewardDelay().result.expectUint(rewardDelay); - - //ccd006CityMining.getBlockWinner(mia.cityId, firstBlock).result.expectUint(2); - ccd006CityMining.getBlockWinner(mia.cityId, claimHeight).result.expectSome().expectUint(winner); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: set-mining-enabled() fails when called directly", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - // act - const block = chain.mineBlock([ccd006CityMining.setMiningEnabled(sender, true)]); - // assert - block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: get-mining-enabled() returns true after deployment", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - // act - // assert - ccd006CityMining.isMiningEnabled().result.expectBool(true); - }, -}); - -// ============================= -// 5. READ-ONLY FUNCTIONS -// ============================= - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if coinbase info isn't set", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - - // act - const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 100); - - // assert - result.expectUint(0); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if the city activation details do not exist", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - // get MIA/NYC city IDs - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - // set city status to activated - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - // set city activation details - // passProposal(PROPOSALS.TEST_CCD005_CITY_DATA_004); - // set city coinbase amounts - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - // set city coinbase thresholds - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - // set city coinbase details - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - - // act - const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 100); - - // assert - result.expectUint(0); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if the block height is before the city's activation height", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - // get MIA/NYC city IDs - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - // set city status to activated - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - // set city activation details - // activation block = 10 - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_004); - // set city coinbase amounts - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - // set city coinbase thresholds - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - // set city coinbase details - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - - // act - const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, 5); - - // assert - result.expectUint(0); - }, -}); - -Clarinet.test({ - name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns the expected amounts", - fn(chain: Chain, accounts: Map<string, Account>) { - // arrange - const sender = accounts.get("deployer")!; - const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); - const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); - // get MIA/NYC city IDs - constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); - // set city activation details - // activation block = 5 - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); - // set city status to activated - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); - // set city coinbase amounts - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); - // set city coinbase thresholds - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); - // set city coinbase details - passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); - - // act - const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(mia.cityId).result.expectTuple(); - // verify coinbase amounts - const expectedAmounts = { - cbaBonus: types.uint(10), - cba1: types.uint(100), - cba2: types.uint(1000), - cba3: types.uint(10000), - cba4: types.uint(100000), - cba5: types.uint(1000000), - cbaDefault: types.uint(10000000), - }; - - // verify coinbase thresholds - const expectedThresholds = { - cbt1: types.uint(50), - cbt2: types.uint(60), - cbt3: types.uint(70), - cbt4: types.uint(80), - cbt5: types.uint(90), - }; - - // verify coinbase details - const expectedDetails = { - bonus: types.uint(20), - epoch: types.uint(1), - }; - - // assert - - // const activation = ccd005CityData.getCityActivationDetails(mia.cityId).result.expectSome().expectTuple(); - // console.log(`activation: ${JSON.stringify(activation)}`); - - // verify coinbase details - assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); - assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); - assertEquals(coinbaseInfo.details.expectSome().expectTuple(), expectedDetails); - - // get coinbase amount based on thresholds - const thresholds = [25, 50, 60, 70, 80, 90]; - let counter = 0; - for (const threshold of thresholds) { - const { result } = ccd006CityMining.getCoinbaseAmount(mia.cityId, threshold); - // console.log(`result for ${threshold}[${counter}]: ${result}`); - assertEquals(result, expectedAmounts[Object.keys(expectedAmounts)[counter]]); - counter++; - } - }, -}); diff --git a/tests/extensions/ccd006-citycoin-mining.test.ts b/tests/extensions/ccd006-citycoin-mining.test.ts index 213c37de..1c311a4f 100644 --- a/tests/extensions/ccd006-citycoin-mining.test.ts +++ b/tests/extensions/ccd006-citycoin-mining.test.ts @@ -29,10 +29,10 @@ const miaTreasuryName = "ccd002-treasury-mia-mining"; /** * Useful for debugging and understanding tests const dumpMiningData = (ccd006CityMining: any, cityId: number, height: number, userId: number, miningStatsAt: object, minerAt: object) => { - console.log("getMiningStatsAtBlock: [height: " + height + "] --> " + ccd006CityMining.getMiningStatsAtBlock(cityId, height).result); - console.log("getMiningStatsAtBlock: [height: " + height + "] --> ", miningStatsAt); - console.log("getMinerAtBlock: [height: " + height + ", userId: " + userId + "] --> " + ccd006CityMining.getMinerAtBlock(cityId, height, userId).result); - console.log("getMinerAtBlock: [height: " + height + ", userId: " + userId + "] --> ", minerAt); + console.log("getMiningStats: [height: " + height + "] --> " + ccd006CityMining.getMiningStats(cityId, height).result); + console.log("getMiningStats: [height: " + height + "] --> ", miningStatsAt); + console.log("getMiner: [height: " + height + ", userId: " + userId + "] --> " + ccd006CityMining.getMiner(cityId, height, userId).result); + console.log("getMiner: [height: " + height + ", userId: " + userId + "] --> ", minerAt); }; */ @@ -42,7 +42,7 @@ const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, claimed: types.bool(miningStatsAt.claimed), miners: types.uint(miningStatsAt.miners), }; - assertEquals(ccd006CityMining.getMiningStatsAtBlock(cityId, height).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiningStats(cityId, height).result.expectTuple(), expectedStats); expectedStats = { commit: types.uint(minerAt.commit), @@ -50,7 +50,7 @@ const checkMiningData = (ccd006CityMining: any, cityId: number, height: number, low: types.uint(minerAt.low), winner: types.bool(minerAt.winner), }; - assertEquals(ccd006CityMining.getMinerAtBlock(cityId, height, userId).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiner(cityId, height, userId).result.expectTuple(), expectedStats); }; const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006CityMining, chain: Chain, sender: Account): any => { @@ -72,7 +72,7 @@ const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006C /** console.log("getCoinbaseAmount : " + coinbase) console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(miaCityId, user1.address, claimHeight).result.expectSome().expectTuple()) - console.log("getMiningStatsAtBlock : ", ccd006CityMining.getMiningStatsAtBlock(miaCityId, claimHeight)) + console.log("getMiningStats : ", ccd006CityMining.getMiningStats(miaCityId, claimHeight)) */ winner = 1; } else if (miningClaimBlock.receipts[1].result === "(ok true)") { @@ -85,7 +85,7 @@ const twoMinersMine = (user1: Account, user2: Account, ccd006CityMining: CCD006C /** console.log("getCoinbaseAmount : " + coinbase) console.log("isBlockWinner : " + ccd006CityMining.isBlockWinner(miaCityId, user2.address, claimHeight).result.expectSome().expectTuple()) - console.log("getMiningStatsAtBlock : ", ccd006CityMining.getMiningStatsAtBlock(miaCityId, claimHeight)) + console.log("getMiningStats : ", ccd006CityMining.getMiningStats(miaCityId, claimHeight)) */ } else { console.log("======== NOONE WINS ========================="); @@ -262,9 +262,12 @@ Clarinet.test({ const sender = accounts.get("deployer")!; const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + // balance is 99999999999992 after ccip-014 deployment + const expectedBalance = 99999999999992; + const entries = [49999999999996, 49999999999996]; // act - const entries = [50000000000000, 50000000000000]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); @@ -272,7 +275,7 @@ Clarinet.test({ const block = chain.mineBlock([ccd006CityMining.mine(sender, miaCityName, entries)]); // assert - ccd002Treasury.getBalanceStx().result.expectUint(100000000000000); + ccd002Treasury.getBalanceStx().result.expectUint(expectedBalance); block.receipts[0].result.expectOk().expectBool(true); }, }); @@ -409,13 +412,13 @@ Clarinet.test({ low: types.uint(0), winner: types.bool(false), }; - assertEquals(ccd006CityMining.getMinerAtBlock(miaCityId, firstBlock, userId).result.expectTuple(), expectedStats2); + assertEquals(ccd006CityMining.getMiner(miaCityId, firstBlock, userId).result.expectTuple(), expectedStats2); const expectedStats = { amount: types.uint(10), claimed: types.bool(false), miners: types.uint(1), }; - assertEquals(ccd006CityMining.getMiningStatsAtBlock(miaCityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedStats); ccd006CityMining.getBlockWinner(miaCityId, firstBlock).result.expectNone(); block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining`, expectedPrintMsg); @@ -470,8 +473,8 @@ Clarinet.test({ winner: types.bool(false), }; for (let i = 0; i < entries.length; i++) { - assertEquals(ccd006CityMining.getMiningStatsAtBlock(miaCityId, firstBlock).result.expectTuple(), expectedStats); - assertEquals(ccd006CityMining.getMinerAtBlock(miaCityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); + assertEquals(ccd006CityMining.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiner(miaCityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); } ccd006CityMining.getBlockWinner(miaCityId, firstBlock).result.expectNone(); @@ -525,7 +528,7 @@ Clarinet.test({ }; // loop through each block to check miner stats for (let i = 0; i < entries.length; i++) { - assertEquals(ccd006CityMining.getMiningStatsAtBlock(miaCityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMining.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedStats); // loop through each user for (let j = 0; j < userIds.length; j++) { // check the data @@ -537,7 +540,7 @@ Clarinet.test({ low: types.uint(j === 0 ? 0 : lastCommit + 1), winner: types.bool(false), }; - assertEquals(ccd006CityMining.getMinerAtBlock(miaCityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); + assertEquals(ccd006CityMining.getMiner(miaCityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); } } @@ -1345,7 +1348,7 @@ Clarinet.test({ claimed: types.bool(false), miners: types.uint(0), }; - assertEquals(ccd006CityMining.getMiningStatsAtBlock(miaCityId, firstBlock).result.expectTuple(), expectedMiningStats); + assertEquals(ccd006CityMining.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedMiningStats); }, }); From 2da68080cd2a967127bcb53270cd1a6a91fa9326 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 13:49:05 -0700 Subject: [PATCH 37/47] fix: add commit hash for ccip-014 --- contracts/proposals/ccip014-pox-3.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index c4ba8ff0..8c3c6480 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -21,7 +21,7 @@ (define-constant CCIP_014 { name: "Upgrade to pox-3", link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", - hash: "TBD", + hash: "0448a33745e8f157214e3da87c512a2cd382dcd2", }) (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places From cf74cd20f12346924f4c0593a64a656fec987943 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Sat, 27 May 2023 14:45:07 -0700 Subject: [PATCH 38/47] fix: disable ccd006-v2 until ccip014 activates --- contracts/extensions/ccd006-citycoin-mining-v2.clar | 2 +- contracts/proposals/ccip014-pox-3.clar | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/extensions/ccd006-citycoin-mining-v2.clar b/contracts/extensions/ccd006-citycoin-mining-v2.clar index 139503bd..10bc7dfc 100644 --- a/contracts/extensions/ccd006-citycoin-mining-v2.clar +++ b/contracts/extensions/ccd006-citycoin-mining-v2.clar @@ -30,7 +30,7 @@ ;; DATA VARS -(define-data-var miningEnabled bool true) +(define-data-var miningEnabled bool false) (define-data-var rewardDelay uint u100) ;; DATA MAPS diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar index 8c3c6480..ff391a9b 100644 --- a/contracts/proposals/ccip014-pox-3.clar +++ b/contracts/proposals/ccip014-pox-3.clar @@ -104,8 +104,9 @@ (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining-v2 "mining-v2")) (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining-v2 "mining-v2")) - ;; disable original mining contract + ;; disable original mining contract and enable v2 (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) + (try! (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled true)) ;; set pool operator to self (try! (contract-call? .ccd011-stacking-payouts set-pool-operator SELF)) From 70ec80c4cadfe38c3377ee764eb39be1baf82005 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 29 May 2023 01:00:45 -0700 Subject: [PATCH 39/47] fix: create clone of ccip-014 as v2 The original proposal failed with err u7006 due to cycle 59 not being complete. This would not allow us to stack for cycle 60, so the related code will be removed and saved for a future proposal. The voting functionality will be removed as well and instead of checking internally if it can be executed, it will call the original ccip-014. --- contracts/proposals/ccip014-pox-3-v2.clar | 310 ++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 contracts/proposals/ccip014-pox-3-v2.clar diff --git a/contracts/proposals/ccip014-pox-3-v2.clar b/contracts/proposals/ccip014-pox-3-v2.clar new file mode 100644 index 00000000..9e71cc8c --- /dev/null +++ b/contracts/proposals/ccip014-pox-3-v2.clar @@ -0,0 +1,310 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) +(impl-trait .ccip-015-trait.ccip-015-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u1400)) +(define-constant ERR_VOTED_ALREADY (err u1401)) +(define-constant ERR_NOTHING_STACKED (err u1402)) +(define-constant ERR_USER_NOT_FOUND (err u1403)) +(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u1404)) +(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u1405)) +(define-constant ERR_NO_CITY_ID (err u1406)) +(define-constant ERR_VOTE_FAILED (err u1407)) + +;; CONSTANTS + +(define-constant CCIP_014 { + name: "Upgrade to pox-3", + link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", + hash: "TBD", +}) + +(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 u8760) ;; 0.8760 or 87.60% +;; MIA votes scaled to make 1 MIA = 1 NYC +;; full calculation available in CCIP-014 + +;; 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)) + (let + ( + (miaId (unwrap! (contract-call? .ccd004-city-registry get-city-id "mia") ERR_PANIC)) + (nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_PANIC)) + (miaBalance (contract-call? .ccd002-treasury-mia-mining get-balance-stx)) + (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) + ) + + ;; check vote complete/passed + (try! (is-executable)) + + ;; update vote variables + (var-set voteEnd block-height) + (var-set voteActive false) + + ;; enable mining v2 treasuries in the DAO + (try! (contract-call? .base-dao set-extensions + (list + {extension: .ccd002-treasury-mia-mining-v2, enabled: true} + {extension: .ccd002-treasury-nyc-mining-v2, enabled: true} + {extension: .ccd006-citycoin-mining-v2, enabled: true} + ) + )) + + ;; allow MIA/NYC in respective treasuries + ;; MAINNET: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 + ;; MAINNET: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 + (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed 'ST1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8WRH7C6H.miamicoin-token-v2 true)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 set-allowed 'STSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1D64KKHQ.newyorkcitycoin-token-v2 true)) + + ;; transfer funds to new treasury extensions + (try! (contract-call? .ccd002-treasury-mia-mining withdraw-stx miaBalance .ccd002-treasury-mia-mining-v2)) + (try! (contract-call? .ccd002-treasury-nyc-mining withdraw-stx nycBalance .ccd002-treasury-nyc-mining-v2)) + + ;; delegate stack the STX in the mining treasuries (up to 50M STX each) + ;; MAINNET: TODO + ;; MAINNET: TODO + (try! (contract-call? .ccd002-treasury-mia-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + + ;; add treasuries to ccd005-city-data + (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining-v2 "mining-v2")) + (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining-v2 "mining-v2")) + + ;; disable original mining contract + (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) + + (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! (and + ;; (>= block-height (var-get voteStart)) + ;; (<= block-height (var-get voteEnd))) + ;; ERR_PROPOSAL_NOT_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 + ;; additional checks could be added here in future proposals + ;; line below revised since vote will start at deployed height + ;; (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) + ;; line below revised since vote will end when proposal executes + ;; (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) + ;; 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_014) +) + +(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 54 / first block BTC 779,450 STX 97,453 + ;; cycle 2 / u4500 used in tests + (cycle54Hash (unwrap! (get-block-hash u4500) none)) + (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle54Amount (get stacked cycle54Data)) + ;; MAINNET: MIA cycle 55 / first block BTC 781,550 STX 99,112 + ;; cycle 3 / u6600 used in tests + (cycle55Hash (unwrap! (get-block-hash u6600) none)) + (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle55Amount (get stacked cycle55Data)) + ;; MIA vote calculation + (avgStacked (/ (+ (scale-up cycle54Amount) (scale-up cycle55Amount)) u2)) + (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle54Amount u0) (> cycle55Amount 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 54 / first block BTC 779,450 STX 97,453 + ;; cycle 2 / u4500 used in tests + (cycle54Hash (unwrap! (get-block-hash u4500) none)) + (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) + (cycle54Amount (get stacked cycle54Data)) + ;; NYC cycle 55 / first block BTC 781,550 STX 99,112 + ;; cycle 3 / u6600 used in tests + (cycle55Hash (unwrap! (get-block-hash u6600) none)) + (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) + (cycle55Amount (get stacked cycle55Data)) + ;; NYC vote calculation + (scaledVote (/ (+ (scale-up cycle54Amount) (scale-up cycle55Amount)) u2)) + ) + ;; check that at least one value is positive + (asserts! (or (> cycle54Amount u0) (> cycle55Amount 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) +) From c5d60f446929e182e8b0ad7d24527cba42ca2128 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 29 May 2023 01:01:21 -0700 Subject: [PATCH 40/47] chore: use latest version of code --- contracts/proposals/ccip014-pox-3-v2.clar | 37 ++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3-v2.clar b/contracts/proposals/ccip014-pox-3-v2.clar index 9e71cc8c..ff391a9b 100644 --- a/contracts/proposals/ccip014-pox-3-v2.clar +++ b/contracts/proposals/ccip014-pox-3-v2.clar @@ -16,10 +16,12 @@ ;; CONSTANTS +(define-constant SELF (as-contract tx-sender)) +(define-constant MISSED_PAYOUT u1) (define-constant CCIP_014 { name: "Upgrade to pox-3", link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", - hash: "TBD", + hash: "0448a33745e8f157214e3da87c512a2cd382dcd2", }) (define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places @@ -93,8 +95,8 @@ (try! (contract-call? .ccd002-treasury-nyc-mining withdraw-stx nycBalance .ccd002-treasury-nyc-mining-v2)) ;; delegate stack the STX in the mining treasuries (up to 50M STX each) - ;; MAINNET: TODO - ;; MAINNET: TODO + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox-fast-pool-v2 + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.pox-fast-pool-v2 (try! (contract-call? .ccd002-treasury-mia-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) (try! (contract-call? .ccd002-treasury-nyc-mining-v2 delegate-stx u50000000000000 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) @@ -102,8 +104,30 @@ (try! (contract-call? .ccd005-city-data add-treasury miaId .ccd002-treasury-mia-mining-v2 "mining-v2")) (try! (contract-call? .ccd005-city-data add-treasury nycId .ccd002-treasury-nyc-mining-v2 "mining-v2")) - ;; disable original mining contract + ;; disable original mining contract and enable v2 (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) + (try! (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled true)) + + ;; set pool operator to self + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator SELF)) + + ;; pay out missed MIA cycles 56, 57, 58, 59 with 1 uSTX each + ;; MAINNET: u56, u57, u58, u59 + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u1 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u2 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u3 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u4 MISSED_PAYOUT))) + + ;; pay out missed NYC cycles 56, 57, 58, 59 with 1 uSTX each + ;; MAINNET: u56, u57, u58, u59 + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u1 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u2 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u3 MISSED_PAYOUT))) + (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u4 MISSED_PAYOUT))) + + ;; set pool operator to Friedger pool + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) (ok true) ) @@ -308,3 +332,8 @@ (define-private (scale-down (a uint)) (/ a VOTE_SCALE_FACTOR) ) + +;; INITIALIZATION + +;; fund proposal with 8 uSTX for payouts from deployer +(stx-transfer? (* MISSED_PAYOUT u8) tx-sender (as-contract tx-sender)) From 366fe9986be63b19d78cdcfec5e95eaca487de58 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 29 May 2023 01:03:04 -0700 Subject: [PATCH 41/47] fix: remove past cycle payout code --- contracts/proposals/ccip014-pox-3-v2.clar | 24 ----------------------- 1 file changed, 24 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3-v2.clar b/contracts/proposals/ccip014-pox-3-v2.clar index ff391a9b..d30b3b34 100644 --- a/contracts/proposals/ccip014-pox-3-v2.clar +++ b/contracts/proposals/ccip014-pox-3-v2.clar @@ -16,8 +16,6 @@ ;; CONSTANTS -(define-constant SELF (as-contract tx-sender)) -(define-constant MISSED_PAYOUT u1) (define-constant CCIP_014 { name: "Upgrade to pox-3", link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", @@ -108,23 +106,6 @@ (try! (contract-call? .ccd006-citycoin-mining set-mining-enabled false)) (try! (contract-call? .ccd006-citycoin-mining-v2 set-mining-enabled true)) - ;; set pool operator to self - (try! (contract-call? .ccd011-stacking-payouts set-pool-operator SELF)) - - ;; pay out missed MIA cycles 56, 57, 58, 59 with 1 uSTX each - ;; MAINNET: u56, u57, u58, u59 - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u1 MISSED_PAYOUT))) - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u2 MISSED_PAYOUT))) - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u3 MISSED_PAYOUT))) - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-mia u4 MISSED_PAYOUT))) - - ;; pay out missed NYC cycles 56, 57, 58, 59 with 1 uSTX each - ;; MAINNET: u56, u57, u58, u59 - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u1 MISSED_PAYOUT))) - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u2 MISSED_PAYOUT))) - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u3 MISSED_PAYOUT))) - (as-contract (try! (contract-call? .ccd011-stacking-payouts send-stacking-reward-nyc u4 MISSED_PAYOUT))) - ;; set pool operator to Friedger pool ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP (try! (contract-call? .ccd011-stacking-payouts set-pool-operator 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) @@ -332,8 +313,3 @@ (define-private (scale-down (a uint)) (/ a VOTE_SCALE_FACTOR) ) - -;; INITIALIZATION - -;; fund proposal with 8 uSTX for payouts from deployer -(stx-transfer? (* MISSED_PAYOUT u8) tx-sender (as-contract tx-sender)) From 1ab5334024daa7892bb840499cd54c93d7a8dab4 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 29 May 2023 01:15:03 -0700 Subject: [PATCH 42/47] fix: remove vote related code --- contracts/proposals/ccip014-pox-3-v2.clar | 244 +--------------------- 1 file changed, 2 insertions(+), 242 deletions(-) diff --git a/contracts/proposals/ccip014-pox-3-v2.clar b/contracts/proposals/ccip014-pox-3-v2.clar index d30b3b34..0c961aa0 100644 --- a/contracts/proposals/ccip014-pox-3-v2.clar +++ b/contracts/proposals/ccip014-pox-3-v2.clar @@ -6,57 +6,13 @@ ;; ERRORS (define-constant ERR_PANIC (err u1400)) -(define-constant ERR_VOTED_ALREADY (err u1401)) (define-constant ERR_NOTHING_STACKED (err u1402)) (define-constant ERR_USER_NOT_FOUND (err u1403)) -(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u1404)) -(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u1405)) (define-constant ERR_NO_CITY_ID (err u1406)) -(define-constant ERR_VOTE_FAILED (err u1407)) - -;; CONSTANTS - -(define-constant CCIP_014 { - name: "Upgrade to pox-3", - link: "https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md", - hash: "0448a33745e8f157214e3da87c512a2cd382dcd2", -}) - -(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 u8760) ;; 0.8760 or 87.60% -;; MIA votes scaled to make 1 MIA = 1 NYC -;; full calculation available in CCIP-014 - -;; 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 +;; supplements CCIP-014 and removes code that fails before cycle 60 starts (define-public (execute (sender principal)) (let ( @@ -69,10 +25,6 @@ ;; check vote complete/passed (try! (is-executable)) - ;; update vote variables - (var-set voteEnd block-height) - (var-set voteActive false) - ;; enable mining v2 treasuries in the DAO (try! (contract-call? .base-dao set-extensions (list @@ -114,202 +66,10 @@ ) ) -(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! (and - ;; (>= block-height (var-get voteStart)) - ;; (<= block-height (var-get voteEnd))) - ;; ERR_PROPOSAL_NOT_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 - ;; additional checks could be added here in future proposals - ;; line below revised since vote will start at deployed height - ;; (asserts! (>= block-height (var-get voteStart)) ERR_PROPOSAL_NOT_ACTIVE) - ;; line below revised since vote will end when proposal executes - ;; (asserts! (>= block-height (var-get voteEnd)) ERR_PROPOSAL_STILL_ACTIVE) - ;; 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)) + (ok true) ) (define-read-only (get-proposal-info) (some CCIP_014) ) - -(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 54 / first block BTC 779,450 STX 97,453 - ;; cycle 2 / u4500 used in tests - (cycle54Hash (unwrap! (get-block-hash u4500) none)) - (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) - (cycle54Amount (get stacked cycle54Data)) - ;; MAINNET: MIA cycle 55 / first block BTC 781,550 STX 99,112 - ;; cycle 3 / u6600 used in tests - (cycle55Hash (unwrap! (get-block-hash u6600) none)) - (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) - (cycle55Amount (get stacked cycle55Data)) - ;; MIA vote calculation - (avgStacked (/ (+ (scale-up cycle54Amount) (scale-up cycle55Amount)) u2)) - (scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE)) - ) - ;; check that at least one value is positive - (asserts! (or (> cycle54Amount u0) (> cycle55Amount 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 54 / first block BTC 779,450 STX 97,453 - ;; cycle 2 / u4500 used in tests - (cycle54Hash (unwrap! (get-block-hash u4500) none)) - (cycle54Data (at-block cycle54Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId))) - (cycle54Amount (get stacked cycle54Data)) - ;; NYC cycle 55 / first block BTC 781,550 STX 99,112 - ;; cycle 3 / u6600 used in tests - (cycle55Hash (unwrap! (get-block-hash u6600) none)) - (cycle55Data (at-block cycle55Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId))) - (cycle55Amount (get stacked cycle55Data)) - ;; NYC vote calculation - (scaledVote (/ (+ (scale-up cycle54Amount) (scale-up cycle55Amount)) u2)) - ) - ;; check that at least one value is positive - (asserts! (or (> cycle54Amount u0) (> cycle55Amount 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) -) From a5d30565d3273650792dc7b7ab20b59c780d26d5 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Mon, 29 May 2023 01:28:47 -0700 Subject: [PATCH 43/47] fix: cleanup code and add check to ccip014 voting data --- Clarinet.toml | 5 +++++ contracts/proposals/ccip014-pox-3-v2.clar | 14 ++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Clarinet.toml b/Clarinet.toml index e88adbb2..318864c3 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -148,6 +148,11 @@ path = "contracts/proposals/ccip014-pox-3.clar" clarity_version = 2 epoch = 2.4 +[contracts.ccip014-pox-3-v2] +path = "contracts/proposals/ccip014-pox-3-v2.clar" +clarity_version = 2 +epoch = 2.4 + # CITYCOINS PROTOCOL TRAITS [contracts.extension-trait] diff --git a/contracts/proposals/ccip014-pox-3-v2.clar b/contracts/proposals/ccip014-pox-3-v2.clar index 0c961aa0..f4e55de5 100644 --- a/contracts/proposals/ccip014-pox-3-v2.clar +++ b/contracts/proposals/ccip014-pox-3-v2.clar @@ -1,7 +1,6 @@ ;; TRAITS (impl-trait .proposal-trait.proposal-trait) -(impl-trait .ccip-015-trait.ccip-015-trait) ;; ERRORS @@ -9,6 +8,7 @@ (define-constant ERR_NOTHING_STACKED (err u1402)) (define-constant ERR_USER_NOT_FOUND (err u1403)) (define-constant ERR_NO_CITY_ID (err u1406)) +(define-constant ERR_VOTE_FAILED (err u1407)) ;; PUBLIC FUNCTIONS @@ -22,10 +22,10 @@ (nycBalance (contract-call? .ccd002-treasury-nyc-mining get-balance-stx)) ) - ;; check vote complete/passed + ;; check vote complete/passed in CCIP-014 (try! (is-executable)) - ;; enable mining v2 treasuries in the DAO + ;; enable mining v2 contracts in the DAO (try! (contract-call? .base-dao set-extensions (list {extension: .ccd002-treasury-mia-mining-v2, enabled: true} @@ -66,10 +66,8 @@ ) ) -(define-read-only (is-executable) - (ok true) -) +;; READ ONLY FUNCTIONS -(define-read-only (get-proposal-info) - (some CCIP_014) +(define-read-only (is-executable) + (contract-call? .ccip014-pox-3 is-executable) ) From 4b043b64d5db8e7055368e9fe2010c9a68fcfdf9 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:22:51 -0700 Subject: [PATCH 44/47] fix: add models and tests for ccip014-pox-3-v2 --- models/proposals/ccip014-pox-3-v2.model.ts | 40 ++ models/proposals/ccip014-pox-3.model.ts | 2 +- tests/proposals/ccip014-pox-3-v2.test.ts | 415 +++++++++++++++++++++ utils/common.ts | 1 + 4 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 models/proposals/ccip014-pox-3-v2.model.ts create mode 100644 tests/proposals/ccip014-pox-3-v2.test.ts diff --git a/models/proposals/ccip014-pox-3-v2.model.ts b/models/proposals/ccip014-pox-3-v2.model.ts new file mode 100644 index 00000000..3da812b2 --- /dev/null +++ b/models/proposals/ccip014-pox-3-v2.model.ts @@ -0,0 +1,40 @@ +import { Chain, Account, ReadOnlyFn } from "../../utils/deps.ts"; + +enum ErrCode { + ERR_PANIC = 1400, + 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 CCIP014Pox3v2 { + name = "ccip014-pox-3-v2"; + 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 + + // read-only functions + + isExecutable() { + return this.callReadOnlyFn("is-executable"); + } + + // read-only function helper + private callReadOnlyFn(method: string, args: Array<any> = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/models/proposals/ccip014-pox-3.model.ts b/models/proposals/ccip014-pox-3.model.ts index 0c805b52..59f25208 100644 --- a/models/proposals/ccip014-pox-3.model.ts +++ b/models/proposals/ccip014-pox-3.model.ts @@ -24,7 +24,7 @@ export class CCIP014Pox3 { // public functions - // execute excluded since can be used with passProposal and CCD001 + // 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); diff --git a/tests/proposals/ccip014-pox-3-v2.test.ts b/tests/proposals/ccip014-pox-3-v2.test.ts new file mode 100644 index 00000000..e89e939a --- /dev/null +++ b/tests/proposals/ccip014-pox-3-v2.test.ts @@ -0,0 +1,415 @@ +import { Account, Clarinet, Chain, types, assertEquals } from "../../utils/deps.ts"; +import { CCD006_REWARD_DELAY, 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 { CCIP014Pox3 } from "../../models/proposals/ccip014-pox-3.model.ts"; +import { CCIP014Pox3v2 } from "../../models/proposals/ccip014-pox-3-v2.model.ts"; + +Clarinet.test({ + name: "ccip-014: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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-014-v2 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP014Pox3v2.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() fails with ERR_VOTE_FAILED if there are more no than yes votes", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox = new CCIP014Pox3(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 in v1 + const votingBlock = chain.mineBlock([ccip014pox.voteOnProposal(user1, false), ccip014pox.voteOnProposal(user2, false)]); + + // execute ccip-014-v2 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); + + // assert + block.receipts[2].result.expectErr().expectUint(CCIP014Pox3v2.ErrCode.ERR_VOTE_FAILED); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there is a single yes vote", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox = new CCIP014Pox3(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-014 + 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([ccip014pox.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(ccip014pox3v2.getVoterInfo(userId)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, userId, false)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, userId, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, userId)); + console.log(ccip014pox3v2.getVoterInfo(userId)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, userId, false)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, userId, true)); + */ + + // execute ccip-014-v2 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there are more yes than no votes", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox = new CCIP014Pox3(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-014 + 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([ccip014pox.voteOnProposal(user1, true), ccip014pox.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(ccip014pox3v2.getVoterInfo(user1Id)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, user1Id, false)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, user1Id, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user1Id)); + console.log(ccip014pox3v2.getVoterInfo(user1Id)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, user1Id, false)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, user1Id, true)); + console.log("\nuser 2 mia:"); + console.log(ccd007CityStacking.getStacker(mia.cityId, cycleId, user2Id)); + console.log(ccip014pox3v2.getVoterInfo(user2Id)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, user2Id, false)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, user2Id, true)); + console.log("\nuser 2 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, user2Id)); + console.log(ccip014pox3v2.getVoterInfo(user2Id)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, user2Id, false)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, user2Id, true)); + */ + + // execute ccip-014-v2 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there are more yes than no votes after a reversal", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccip014pox = new CCIP014Pox3(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-014 + 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([ccip014pox.voteOnProposal(user1, false), ccip014pox.voteOnProposal(user2, true)]); + + // switch yes and no vote + const votingBlockReverse = chain.mineBlock([ccip014pox.voteOnProposal(user1, true), ccip014pox.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-014-v2 + const block = passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); + + // assert + //console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + block.receipts[2].result.expectOk().expectUint(3); + }, +}); + +Clarinet.test({ + name: "ccip-014: after upgrade mining disabled, mining-v2 enabled, mining and stacking claims work as expected", + fn(chain: Chain, accounts: Map<string, Account>) { + // 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 ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const ccip014pox = new CCIP014Pox3(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-014 + const constructBlock = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_001); + + // mine to put funds in the mining treasury + const miningBlockBefore = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + //console.log(`\nminingBlockBefore:\n${JSON.stringify(miningBlockBefore, null, 2)}`); + miningBlockBefore.receipts[0].result.expectOk().expectBool(true); + miningBlockBefore.receipts[1].result.expectOk().expectBool(true); + + // mine in v2 before the upgrade, fails with ERR_INVALID_TREASURY + const miningBlockV2Before = chain.mineBlock([ccd006CityMiningV2.mine(sender, mia.cityName, miningEntries), ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + miningBlockV2Before.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); + miningBlockV2Before.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_TREASURY); + + // 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, lockPeriod), ccd007CityStacking.stack(user2, nyc.cityName, amountStacked, lockPeriod)]); + stackingBlock.receipts[0].result.expectOk().expectBool(true); + stackingBlock.receipts[1].result.expectOk().expectBool(true); + stackingBlock.receipts[2].result.expectOk().expectBool(true); + stackingBlock.receipts[3].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); + + // is-cycle-paid returns false for cycles 1-4 for MIA + ccd007CityStacking.isCyclePaid(mia.cityId, 1).result.expectBool(false); + ccd007CityStacking.isCyclePaid(mia.cityId, 2).result.expectBool(false); + ccd007CityStacking.isCyclePaid(mia.cityId, 3).result.expectBool(false); + ccd007CityStacking.isCyclePaid(mia.cityId, 4).result.expectBool(false); + + // is-cycle-paid returns false for cycles 1-4 for NYC + ccd007CityStacking.isCyclePaid(nyc.cityId, 1).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 2).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 3).result.expectBool(false); + ccd007CityStacking.isCyclePaid(nyc.cityId, 4).result.expectBool(false); + + // claim-stacking-reward before, fails with ERR_NOTHING_TO_CLAIM + // since cycle is not paid out yet + const claimStackingRewardBefore = chain.mineBlock([ccd007CityStacking.claimStackingReward(user1, mia.cityName, 2), ccd007CityStacking.claimStackingReward(user1, nyc.cityName, 2), ccd007CityStacking.claimStackingReward(user2, mia.cityName, 2), ccd007CityStacking.claimStackingReward(user2, nyc.cityName, 2)]); + claimStackingRewardBefore.receipts[0].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardBefore.receipts[1].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardBefore.receipts[2].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + claimStackingRewardBefore.receipts[3].result.expectErr().expectUint(CCD007CityStacking.ErrCode.ERR_NOTHING_TO_CLAIM); + + // execute single yes vote + const votingBlock = chain.mineBlock([ccip014pox.voteOnProposal(user1, true)]); + votingBlock.receipts[0].result.expectOk().expectBool(true); + + // execute ccip-014-v2 + const executeBlock = passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); + executeBlock.receipts[2].result.expectOk().expectUint(3); + + /* 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(ccip014pox3v2.getVoterInfo(userId)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, userId, false)); + console.log(ccip014pox3v2.getMiaVote(mia.cityId, userId, true)); + console.log("\nuser 1 nyc:"); + console.log(ccd007CityStacking.getStacker(nyc.cityId, cycleId, userId)); + console.log(ccip014pox3v2.getVoterInfo(userId)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, userId, false)); + console.log(ccip014pox3v2.getNycVote(nyc.cityId, userId, true)); + console.log(`\nexecute block:\n${JSON.stringify(block, null, 2)}`); + */ + + // act + + // mine in v1 after the upgrade, fails with ERR_MINING_DISABLED + const miningBlockAfter = chain.mineBlock([ccd006CityMining.mine(sender, mia.cityName, miningEntries), ccd006CityMining.mine(sender, nyc.cityName, miningEntries)]); + miningBlockAfter.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + miningBlockAfter.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + + // mine in v2 after the upgrade + const miningBlockV2After = chain.mineBlock([ccd006CityMiningV2.mine(sender, mia.cityName, miningEntries), ccd006CityMiningV2.mine(sender, nyc.cityName, miningEntries)]); + //console.log(`\nminingBlockV2After:\n${JSON.stringify(miningBlockV2After, null, 2)}`); + miningBlockV2After.receipts[0].result.expectOk().expectBool(true); + miningBlockV2After.receipts[0].result.expectOk().expectBool(true); + + // fast forward so claims are valid + chain.mineEmptyBlock(CCD006_REWARD_DELAY + 1); + + // pass proposal to set city info for claims + passProposal(chain, accounts, PROPOSALS.TEST_CCIP014_POX3_002); + + const claimBlockHeight = miningBlockBefore.height - 1; + const claimBlockHeightV2 = miningBlockV2After.height - 1; + //console.log(`\nclaim block height: ${claimBlockHeight}`); + //console.log(`\nclaim block height v2: ${claimBlockHeightV2}`); + + // test claim in v1 after upgrade + const miningClaimAfter = chain.mineBlock([ccd006CityMining.claimMiningReward(sender, mia.cityName, claimBlockHeight)]); + //console.log(`\nmining claim after:\n${JSON.stringify(miningClaimAfter, null, 2)}`); + miningClaimAfter.receipts[0].result.expectOk().expectBool(true); + + // test claim in v2 after upgrade + const miningClaimV2After = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, mia.cityName, claimBlockHeightV2)]); + //console.log(`\nmining claim v2 after:\n${JSON.stringify(miningClaimV2After, null, 2)}`); + miningClaimV2After.receipts[0].result.expectOk().expectBool(true); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index 6906c27f..11e1ba17 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -33,6 +33,7 @@ export const PROPOSALS = { CCIP_012: ADDRESS.concat(".ccip012-bootstrap"), CCIP_013: ADDRESS.concat(".ccip013-migration"), CCIP_014: ADDRESS.concat(".ccip014-pox-3"), + CCIP_014_V2: ADDRESS.concat(".ccip014-pox-3-v2"), 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"), From f1ff69ef773a42f52c8aa5acf1b2f7ed38f2f784 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:13:01 -0700 Subject: [PATCH 45/47] fix: add ccd006-citycoin-mining-v2 tests --- Clarinet.toml | 19 +- tests/base-dao.test.ts | 4 + .../proposals/test-ccd005-city-data-020.clar | 13 + .../test-ccd006-citycoin-mining-v2-006.clar | 9 + .../test-ccd006-citycoin-mining-v2-007.clar | 13 + .../ccd006-citycoin-mining-v2.test.ts | 1458 +++++++++++++++++ .../extensions/ccd006-citycoin-mining.test.ts | 8 +- utils/common.ts | 52 +- 8 files changed, 1566 insertions(+), 10 deletions(-) create mode 100644 tests/contracts/proposals/test-ccd005-city-data-020.clar create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-006.clar create mode 100644 tests/contracts/proposals/test-ccd006-citycoin-mining-v2-007.clar create mode 100644 tests/extensions/ccd006-citycoin-mining-v2.test.ts diff --git a/Clarinet.toml b/Clarinet.toml index 318864c3..244f32fc 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -343,6 +343,9 @@ path = "tests/contracts/proposals/test-ccd005-city-data-018.clar" [contracts.test-ccd005-city-data-019] path = "tests/contracts/proposals/test-ccd005-city-data-019.clar" +[contracts.test-ccd005-city-data-020] +path = "tests/contracts/proposals/test-ccd005-city-data-020.clar" + [contracts.test-ccd006-citycoin-mining-001] path = "tests/contracts/proposals/test-ccd006-citycoin-mining-001.clar" @@ -359,19 +362,25 @@ path = "tests/contracts/proposals/test-ccd006-citycoin-mining-004.clar" path = "tests/contracts/proposals/test-ccd006-citycoin-mining-005.clar" [contracts.test-ccd006-citycoin-mining-v2-001] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-001.clar" +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-001.clar" [contracts.test-ccd006-citycoin-mining-v2-002] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-002.clar" +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-002.clar" [contracts.test-ccd006-citycoin-mining-v2-003] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-003.clar" +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-003.clar" [contracts.test-ccd006-citycoin-mining-v2-004] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-004.clar" +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-004.clar" [contracts.test-ccd006-citycoin-mining-v2-005] -path = "tests/contracts/proposals/test-ccd006-citycoin-mining-005.clar" +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-005.clar" + +[contracts.test-ccd006-citycoin-mining-v2-006] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-006.clar" + +[contracts.test-ccd006-citycoin-mining-v2-007] +path = "tests/contracts/proposals/test-ccd006-citycoin-mining-v2-007.clar" [contracts.test-ccd007-citycoin-stacking-001] path = "tests/contracts/proposals/test-ccd007-citycoin-stacking-001.clar" diff --git a/tests/base-dao.test.ts b/tests/base-dao.test.ts index 38436934..d87fe120 100644 --- a/tests/base-dao.test.ts +++ b/tests/base-dao.test.ts @@ -204,6 +204,10 @@ Clarinet.test({ for (const ext of Object.values(EXTENSIONS)) { if (ext === EXTENSIONS.CCD008_CITY_ACTIVATION) continue; // temporarily skip if (ext === EXTENSIONS.CCD006_CITYCOIN_MINING_V2) continue; // skip, not enabled until CCIP-014 + if (ext === EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2) continue; // skip, not enabled until CCIP-014 + if (ext === EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2) continue; // skip, not enabled until CCIP-014 + //console.log("ext:", ext); + //console.log("enabled:", baseDao.isExtension(ext).result); baseDao.isExtension(ext).result.expectBool(true); } }, diff --git a/tests/contracts/proposals/test-ccd005-city-data-020.clar b/tests/contracts/proposals/test-ccd005-city-data-020.clar new file mode 100644 index 00000000..b1cbc868 --- /dev/null +++ b/tests/contracts/proposals/test-ccd005-city-data-020.clar @@ -0,0 +1,13 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd005-city-data add-treasury u1 .mia-treasury "mining-v2")) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-006.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-006.clar new file mode 100644 index 00000000..b1974795 --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-006.clar @@ -0,0 +1,9 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (contract-call? .base-dao set-extension .ccd006-citycoin-mining-v2 false) +) diff --git a/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-007.clar b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-007.clar new file mode 100644 index 00000000..30edd013 --- /dev/null +++ b/tests/contracts/proposals/test-ccd006-citycoin-mining-v2-007.clar @@ -0,0 +1,13 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd006-citycoin-mining-v2 set-reward-delay u50)) + (ok true) + ) +) diff --git a/tests/extensions/ccd006-citycoin-mining-v2.test.ts b/tests/extensions/ccd006-citycoin-mining-v2.test.ts new file mode 100644 index 00000000..3e823e83 --- /dev/null +++ b/tests/extensions/ccd006-citycoin-mining-v2.test.ts @@ -0,0 +1,1458 @@ +/** + * Test class is structured; + * 0. AUTHORIZATION CHECKS + * 1. mine + * 2. claim-mining-reward + * 3. reward-delay + * 4. mining status + * 5. read-only functions + */ +import { Account, assert, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; +import { constructAndPassProposal, EXTENSIONS, passCcip014, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { CCD002Treasury } from "../../models/extensions/ccd002-treasury.model.ts"; +import { CCD003UserRegistry } from "../../models/extensions/ccd003-user-registry.model.ts"; +import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.ts"; +import { CCD006CityMining } from "../../models/extensions/ccd006-citycoin-mining.model.ts"; +import { CCD010CoreV2Adapter } from "../../models/extensions/ccd010-core-v2-adapter.model.ts"; +import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; + +// ============================= +// INTERNAL DATA / FUNCTIONS +// ============================= +const rewardDelay = 100; +const miaCityId = 1; +const miaCityName = "mia"; +const miaTreasuryId = 1; +const miaMiningTreasuryName = "mining"; +const miaTreasuryName = "ccd002-treasury-mia-mining"; +const miaTreasuryNameV2 = "ccd002-treasury-mia-mining-v2"; + +/** + * Useful for debugging and understanding tests +const dumpMiningData = (ccd006CityMining: any, cityId: number, height: number, userId: number, miningStatsAt: object, minerAt: object) => { + console.log("getMiningStats: [height: " + height + "] --> " + ccd006CityMiningV2.getMiningStats(cityId, height).result); + console.log("getMiningStats: [height: " + height + "] --> ", miningStatsAt); + console.log("getMiner: [height: " + height + ", userId: " + userId + "] --> " + ccd006CityMiningV2.getMiner(cityId, height, userId).result); + console.log("getMiner: [height: " + height + ", userId: " + userId + "] --> ", minerAt); +}; + */ + +const checkMiningData = (ccd006CityMiningV2: any, cityId: number, height: number, userId: number, miningStatsAt: any, minerAt: any) => { + let expectedStats: any = { + amount: types.uint(miningStatsAt.amount), + claimed: types.bool(miningStatsAt.claimed), + miners: types.uint(miningStatsAt.miners), + }; + assertEquals(ccd006CityMiningV2.getMiningStats(cityId, height).result.expectTuple(), expectedStats); + + expectedStats = { + commit: types.uint(minerAt.commit), + high: types.uint(minerAt.high), + low: types.uint(minerAt.low), + winner: types.bool(minerAt.winner), + }; + assertEquals(ccd006CityMiningV2.getMiner(cityId, height, userId).result.expectTuple(), expectedStats); +}; + +const twoMinersMine = (user1: Account, user2: Account, ccd006CityMiningV2: CCD006CityMining, chain: Chain, sender: Account): any => { + const entries: number[] = [10]; + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries), ccd006CityMiningV2.mine(user2, miaCityName, entries)]); + const claimHeight = miningBlock.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + const miningClaimBlock = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user1, miaCityName, claimHeight), ccd006CityMiningV2.claimMiningReward(user2, miaCityName, claimHeight)]); + + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${miaTreasuryNameV2}`); + miningBlock.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${miaTreasuryNameV2}`); + let winner = 0; + let coinbase = 0; + if (miningClaimBlock.receipts[0].result === "(ok true)") { + //console.log("======== USER 1 WINS =========================") + ccd006CityMiningV2.getBlockWinner(miaCityId, claimHeight).result.expectSome().expectUint(1); + coinbase = Number(ccd006CityMiningV2.getCoinbaseAmount(miaCityId, claimHeight).result.substring(1)); + miningClaimBlock.receipts[0].result.expectOk().expectBool(true); + /** + console.log("getCoinbaseAmount : " + coinbase) + console.log("isBlockWinner : " + ccd006CityMiningV2.isBlockWinner(miaCityId, user1.address, claimHeight).result.expectSome().expectTuple()) + console.log("getMiningStats : ", ccd006CityMiningV2.getMiningStats(miaCityId, claimHeight)) + */ + winner = 1; + } else if (miningClaimBlock.receipts[1].result === "(ok true)") { + //console.log("======== USER 2 WINS =========================") + ccd006CityMiningV2.getBlockWinner(miaCityId, claimHeight).result.expectSome().expectUint(2); + //miningClaimBlock.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimBlock.receipts[1].result.expectOk().expectBool(true); + coinbase = Number(ccd006CityMiningV2.getCoinbaseAmount(miaCityId, claimHeight).result.substring(1)); + winner = 2; + /** + console.log("getCoinbaseAmount : " + coinbase) + console.log("isBlockWinner : " + ccd006CityMiningV2.isBlockWinner(miaCityId, user2.address, claimHeight).result.expectSome().expectTuple()) + console.log("getMiningStats : ", ccd006CityMiningV2.getMiningStats(miaCityId, claimHeight)) + */ + } else { + console.log("======== NOONE WINS ========================="); + return 3; + } + return { miningBlock, miningClaimBlock, claimHeight, winner, coinbase }; +}; + +// ============================= +// 0. AUTHORIZATION CHECKS +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: is-dao-or-extension() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + + // assert + ccd006CityMiningV2.isDaoOrExtension().result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +// Extension callback + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: callback() succeeds when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const { receipts } = chain.mineBlock([ccd006CityMiningV2.callback(sender, "test")]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectOk().expectBool(true); + }, +}); + +// ============================= +// 1. mine +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city is not registered", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries = [10, 10]; + const { receipts } = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city is not active", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries = [10, 10]; + const upgrade = passCcip014(chain, accounts); + // console.log("upgrade: ", upgrade); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_003); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + //console.log("block: ", block); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INACTIVE_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if mining contract is not a valid dao extension", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries = [10, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_006); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD003UserRegistry.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if user has insufficient balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries = [100000000000001]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NOT_ENOUGH_FUNDS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() succeeds if user's cumulative commit uses their exact balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const wallet_2 = accounts.get("wallet_2")!; + const ccd002TreasuryV2 = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + // balance is 100000000000000 by default + 50000000 from transition to v2 + const expectedBalance = 100000050000000; + const entries = [50000000000000, 50000000000000]; + + // act + ccd002TreasuryV2.getBalanceStx().result.expectUint(0); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(wallet_2, miaCityName, entries)]); + + // assert + ccd002TreasuryV2.getBalanceStx().result.expectUint(expectedBalance); + block.receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if user's cumulative commit leaves insufficient balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries = [50000000000000, 50000000000000, 1]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + ccd002Treasury.getBalanceStx().result.expectUint(0); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NOT_ENOUGH_FUNDS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if city is inactive", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries = [10, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_003); + + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INACTIVE_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if called with no commit amounts", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = []; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_COMMITS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if a commit amount in the list is zero", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10, 10, 10, 0, 10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_COMMITS); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() succeeds and mines 1 block for 1 user", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(user, miaCityName, entries)]); + + // assert + const firstBlock = block.height - 1; + const lastBlock = firstBlock; + const totalAmount = 10; + const totalBlocks = 1; + const userId = 2; + + block.receipts[0].result.expectOk().expectBool(true); + + block.receipts[0].events.expectSTXTransferEvent(totalAmount, user.address, `${sender.address}.${miaTreasuryNameV2}`); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + //console.log(block.receipts[0].events[1].contract_event.value) + //ccd006CityMiningV2.isBlockWinner(miaCityId, user.address, firstBlock).result.expectBool(true) + const expectedStats2 = { + commit: types.uint(totalAmount), + high: types.uint(totalAmount), + low: types.uint(0), + winner: types.bool(false), + }; + assertEquals(ccd006CityMiningV2.getMiner(miaCityId, firstBlock, userId).result.expectTuple(), expectedStats2); + const expectedStats = { + amount: types.uint(totalAmount), + claimed: types.bool(false), + miners: types.uint(1), + }; + assertEquals(ccd006CityMiningV2.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedStats); + ccd006CityMiningV2.getBlockWinner(miaCityId, firstBlock).result.expectNone(); + + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + block.receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() succeeds and mines 200 blocks for 1 user", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const userId = 2; + const commitAmount = 10; + const numberOfBlocks = 200; + const entries = new Array<number>(numberOfBlocks).fill(commitAmount); + const totalAmount = entries.reduce((a, b) => a + b, 0); + const totalBlocks = entries.length; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(user, miaCityName, entries)]); + + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[0].events.expectSTXTransferEvent(totalAmount, user.address, `${sender.address}.${miaTreasuryNameV2}`); + + // mining stats at block + const expectedStats = { + amount: types.uint(commitAmount), + claimed: types.bool(false), + miners: types.uint(1), + }; + // miner stats at each block + const expectedMinerStats = { + commit: types.uint(commitAmount), + high: types.uint(commitAmount), + low: types.uint(0), + winner: types.bool(false), + }; + for (let i = 0; i < entries.length; i++) { + assertEquals(ccd006CityMiningV2.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedStats); + assertEquals(ccd006CityMiningV2.getMiner(miaCityId, firstBlock + i, userId).result.expectTuple(), expectedMinerStats); + } + + ccd006CityMiningV2.getBlockWinner(miaCityId, firstBlock).result.expectNone(); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + block.receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() successfully mines 100 blocks for 3 users", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!]; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const userIds = [2, 3, 4]; + const commitAmount = 100; + const numberOfBlocks = 100; + const entries = new Array<number>(numberOfBlocks).fill(commitAmount); + const totalCommit = entries.reduce((a, b) => a + b, 0); + const totalBlocks = entries.length; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(users[0], miaCityName, entries), ccd006CityMiningV2.mine(users[1], miaCityName, entries), ccd006CityMiningV2.mine(users[2], miaCityName, entries)]); + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + + for (let i = 0; i < userIds.length; i++) { + // check that each event succeeded + block.receipts[i].result.expectOk().expectBool(true); + // check that each event transferred the correct amount to the correct address + block.receipts[i].events.expectSTXTransferEvent(totalCommit, users[i].address, `${sender.address}.${miaTreasuryNameV2}`); + } + + // mining stats at block + const expectedStats = { + amount: types.uint(commitAmount * users.length), + claimed: types.bool(false), + miners: types.uint(users.length), + }; + // loop through each block to check miner stats + for (let i = 0; i < entries.length; i++) { + assertEquals(ccd006CityMiningV2.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedStats); + // loop through each user + for (let j = 0; j < userIds.length; j++) { + // check the data + const lastCommit = commitAmount * j; + const currentCommit = commitAmount * (j + 1); + const expectedMinerStats = { + commit: types.uint(commitAmount), + high: types.uint(currentCommit), + low: types.uint(j === 0 ? 0 : lastCommit + 1), + winner: types.bool(false), + }; + assertEquals(ccd006CityMiningV2.getMiner(miaCityId, firstBlock + i, userIds[j]).result.expectTuple(), expectedMinerStats); + } + } + + // check the print message for each user + for (let i = 0; i < userIds.length; i++) { + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalCommit)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userIds[i])}}`; + block.receipts[i].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + block.receipts[i].result.expectOk().expectBool(true); + } + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if user has already mined", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002TreasuryV2 = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const totalAmount = 30; + const totalBlocks = 3; + const userId = 1; + const entries: number[] = [10, 10, 10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries), ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_MINED); + block.receipts[0].events.expectSTXTransferEvent(totalAmount, sender.address, `${sender.address}.${miaTreasuryNameV2}`); + //console.log("miningBlock:", block.receipts[0].events[0].contract_event.value); + + // block.receipts[1].events.expectSTXTransferEvent(totalAmount, sender.address, `${sender.address}.${miaTreasuryNameV2}`); + + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + // Expecting ERR_ALREADY_MINED if same user tried mine twice at same height ? + block.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_MINED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() keeps track of mining stats for 4 users mining 3 blocks concurrently", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const users = [accounts.get("wallet_1")!, accounts.get("wallet_2")!, accounts.get("wallet_3")!, accounts.get("wallet_4")!]; + + const ccd002TreasuryV2 = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const ccd003UserRegistry = new CCD003UserRegistry(chain, sender, "ccd003-user-registry"); + + const totalAmount = 120; + const totalBlocks = 3; + const entries: number[] = [10, 10, 10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + const block = chain.mineBlock([ccd006CityMiningV2.mine(users[0], miaCityName, entries), ccd006CityMiningV2.mine(users[1], miaCityName, entries), ccd006CityMiningV2.mine(users[2], miaCityName, entries), ccd006CityMiningV2.mine(users[3], miaCityName, entries)]); + const firstBlock = block.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + // assert + let miningStatsAt, minerAt; + for (let idx = 0; idx < 4; idx++) { + block.receipts[idx].result.expectOk().expectBool(true); + ccd003UserRegistry + .getUserId(users[idx].address) + .result.expectSome() + .expectUint(idx + 2); + ccd003UserRegistry + .getUser(idx + 2) + .result.expectSome() + .expectPrincipal(users[idx].address); + block.receipts[idx].events.expectSTXTransferEvent(30, users[idx].address, `${sender.address}.${miaTreasuryNameV2}`); + + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount / 4)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(idx + 2)}}`; + //console.log(idx, block.receipts[idx].events); + block.receipts[idx].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + } + + for (let blockIdx = 0; blockIdx < entries.length; blockIdx++) { + // for 3 blocks + for (let userIdx = 0; userIdx < users.length; userIdx++) { + // for 4 users + miningStatsAt = { + amount: types.uint(users.length * entries[blockIdx]), + claimed: types.bool(false), + miners: types.uint(users.length), + }; + minerAt = { + commit: types.uint(10), + high: types.uint(10 * (userIdx + 1)), + low: types.uint(userIdx === 0 ? 0 : userIdx * 10 + 1), + winner: types.bool(false), + }; + assertEquals(ccd006CityMiningV2.getMiningStats(miaCityId, firstBlock + blockIdx).result.expectTuple(), miningStatsAt); + assertEquals(ccd006CityMiningV2.getMiner(miaCityId, firstBlock + blockIdx, userIdx + 2).result.expectTuple(), minerAt); + } + } + // check high values + ccd006CityMiningV2.getHighValue(miaCityId, firstBlock).result.expectUint(totalAmount / 3); + ccd006CityMiningV2.getHighValue(miaCityId, lastBlock).result.expectUint(totalAmount / 3); + ccd006CityMiningV2.getHighValue(miaCityId, lastBlock + 1).result.expectUint(0); + }, +}); + +// ============================= +// 2. claim-mining-reward +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: is-block-winner() correctly identifies winning miner who has not claimed", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + const totalAmount = 10; + const totalBlocks = 1; + const entries: number[] = [10]; + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(0); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries)]); + // console.log(`miningBlock:\n${JSON.stringify(miningBlock, null, 2)}}`); + const claimHeight = miningBlock.height - 1; + const lastBlock = claimHeight + totalBlocks - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + // assert + miningBlock.receipts[0].result.expectOk().expectBool(true); + + // Check stx transfer events + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${miaTreasuryNameV2}`); + + // Check mining events + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + const expected = { + claimed: types.bool(false), + winner: types.bool(true), + }; + const isBlockWinner = ccd006CityMiningV2.isBlockWinner(miaCityId, user1.address, claimHeight); + //console.log(JSON.stringify(isBlockWinner, null, 2)); + // console.log(`isBlockWinner:\n${JSON.stringify(isBlockWinner, null, 2)}}`); + assertEquals(isBlockWinner.result.expectSome().expectTuple(), expected); + // is-block-winner calculates the winning status of given user. + // get-block-winner reads it from the map which is written by claim-mining-reward. + // so user1 is not returned by the following. This is correct, since the map isn't written + // to until the reward is claimed, + // we would expect the value to be none here while the output of isBlockWinner() above + // will be (some (claimed false) (winner true)). + ccd006CityMiningV2.getBlockWinner(miaCityId, claimHeight).result.expectNone(); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: is-block-winner() correctly identifies winning miner who has claimed", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + const totalAmount = 10; + const totalBlocks = 1; + const entries: number[] = [10]; + gt.getBalance(user1.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING).result.expectOk().expectUint(0); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passCcip014(chain, accounts); + + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries)]); + const miningHeight = miningBlock.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + const miningClaimBlock = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user1, miaCityName, miningHeight)]); + + // assert + miningBlock.receipts[0].result.expectOk().expectBool(true); + // Check mining event + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(miningHeight)}, lastBlock: ${types.uint(miningHeight)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + // Check mining claim event + expectedPrintMsg = `{cityId: u1, cityName: "mia", claimHeight: ${types.uint(miningHeight)}, event: "mining-claim", userId: ${types.uint(2)}}`; + miningClaimBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + // Check stx transfer events + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${miaTreasuryNameV2}`); + const expected = { + claimed: types.bool(true), + winner: types.bool(true), + }; + assertEquals(ccd006CityMiningV2.isBlockWinner(miaCityId, user1.address, miningHeight).result.expectSome().expectTuple(), expected); + ccd006CityMiningV2.getBlockWinner(miaCityId, miningHeight).result.expectSome().expectUint(2); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible for an unknown city", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const { receipts } = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, miaCityName, 50)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(100); + receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_CITY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if current tip height is less than maturity height ", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + const block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, miaCityName, 50)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(100); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_REWARD_IMMATURE); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if current tip height is equal to maturity height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + const claimBlock = passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + chain.mineEmptyBlock(rewardDelay - 1); + const block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, miaCityName, claimBlock.height)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_REWARD_IMMATURE); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() is not possible if user is not registered", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + const claimHeight = 5; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + const block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(sender, miaCityName, claimHeight)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_USER); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() returns ERR_NO_MINER_DATA if user did not mine in that block", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + const claimHeight = 6; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + const block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user, miaCityName, claimHeight)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if a user tries claiming another users rewards", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_002); + + let block = chain.mineBlock([ccd006CityMiningV2.mine(sender, miaCityName, entries)]); + const claimHeight = block.height - 1; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user, miaCityName, claimHeight)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_USER); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if there is nothing to mint at the given claim height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + let block = chain.mineBlock([ccd006CityMiningV2.mine(user, miaCityName, entries)]); + const claimHeight = block.height - 1; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user, miaCityName, claimHeight)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails with ERR_NOTHING_TO_MINT if the coinbase amounts have not been set for the given city", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + + let block = chain.mineBlock([ccd006CityMiningV2.mine(user, miaCityName, entries)]); + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[0].result.expectOk().expectBool(true); + const firstBlock = block.height - 1; + const lastBlock = firstBlock; + const totalAmount = 10; + const totalBlocks = 1; + const userId = 2; + block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${miaTreasuryNameV2}`); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; + //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; + // dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (1), miningStatsAt, minerAt); + + const claimHeight = block.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user, miaCityName, claimHeight)]); + + // assert + + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails with ERR_NOTHING_TO_MINT if called at the wrong claim height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + + let block = chain.mineBlock([ccd006CityMiningV2.mine(user, miaCityName, entries)]); + block.receipts[0].result.expectOk().expectBool(true); + block.receipts[0].result.expectOk().expectBool(true); + const firstBlock = block.height - 1; + const lastBlock = firstBlock; + const totalAmount = 10; + const totalBlocks = 1; + const userId = 2; + block.receipts[0].events.expectSTXTransferEvent(10, user.address, `${sender.address}.${miaTreasuryNameV2}`); + const expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(userId)}}`; + block.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; + //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; + //dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (1), miningStatsAt, minerAt); + + const claimHeight = block.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user, miaCityName, claimHeight)]); + + // assert + + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if user is not the winner or if there is nothing to mint", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const totalAmount = 10; + const totalBlocks = 1; + const entries: number[] = [10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + + const block1 = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries), ccd006CityMiningV2.mine(user2, miaCityName, entries)]); + const firstBlock = block1.height - 1; + const lastBlock = firstBlock + entries.length - 1; + + chain.mineEmptyBlock(rewardDelay); + const block2 = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user1, miaCityName, firstBlock), ccd006CityMiningV2.claimMiningReward(user2, miaCityName, firstBlock)]); + + // assert + + block1.receipts[0].result.expectOk().expectBool(true); + block1.receipts[1].result.expectOk().expectBool(true); + + if (block2.receipts[0].result === "(err u6015)") { + //console.log("USER 2 WINS"); + block2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + block2.receipts[1].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + } else { + //console.log("USER 1 WINS"); + block2.receipts[0].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); + block2.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + } + + block1.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${miaTreasuryNameV2}`); + block1.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${miaTreasuryNameV2}`); + + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + block1.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(firstBlock)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(3)}}`; + block1.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + //dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (1), miningStatsAt, minerAt); + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + + //ccd006CityMiningV2.getBlockWinner(miaCityId, firstBlock).result.expectUint(2); + ccd006CityMiningV2.getBlockWinner(miaCityId, firstBlock).result.expectNone(); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() user makes successful claim", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const totalAmount = 10; + const totalBlocks = 1; + const entries = [10]; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passCcip014(chain, accounts); + + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries), ccd006CityMiningV2.mine(user2, miaCityName, entries)]); + // console.log(JSON.stringify(miningBlock, null, 2)); + const claimHeight = miningBlock.height - 1; + const lastBlock = claimHeight + totalBlocks - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + const miningClaimUser1 = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user1, miaCityName, claimHeight)]); + const miningClaimUser2 = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user2, miaCityName, claimHeight)]); + + // assert + miningBlock.receipts[0].result.expectOk().expectBool(true); + miningBlock.receipts[0].events.expectSTXTransferEvent(totalAmount, user1.address, `${sender.address}.${miaTreasuryNameV2}`); + miningBlock.receipts[1].result.expectOk().expectBool(true); + miningBlock.receipts[1].events.expectSTXTransferEvent(totalAmount, user2.address, `${sender.address}.${miaTreasuryNameV2}`); + + //const miningStatsAt = { amount: 10, claimed: false, miners: 1 }; + //const minerAt = { commit: 10, high: 11, low: 0, winner: false }; + //dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (1), miningStatsAt, minerAt); + //dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (2), miningStatsAt, minerAt); + + //console.log(`miningClaimUser1:\n${JSON.stringify(miningClaimUser1, null, 2)}\n`); + //console.log(`miningClaimUser2:\n${JSON.stringify(miningClaimUser2, null, 2)}\n`); + + let winner: number; + + if (miningClaimUser2.receipts[0].result === "(err u6015)") { + //console.log("USER 1 WINS"); + miningClaimUser1.receipts[0].result.expectOk().expectBool(true); + miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + winner = 2; + } else { + //console.log("USER 2 WINS"); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimUser2.receipts[0].result.expectOk().expectBool(true); + winner = 3; + } + + // {cityId: u1, cityName: \"mia\", cityTreasury: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining, event: \"mining\", firstBlock: u10, lastBlock: u10, totalAmount: u10, totalBlocks: u1, userId: u1}"} + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + // console.log(JSON.stringify(miningBlock.receipts[0].events), null, 2); + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(3)}}`; + miningBlock.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + //dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (1), miningStatsAt, minerAt); + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + + //ccd006CityMiningV2.getBlockWinner(miaCityId, firstBlock).result.expectUint(2); + ccd006CityMiningV2.getBlockWinner(miaCityId, claimHeight).result.expectSome().expectUint(winner); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() fails if user claims at incorrect height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passCcip014(chain, accounts); + + // assert + const entries = [10]; + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries)]); + const claimHeight = miningBlock.height - 1; + chain.mineEmptyBlock(rewardDelay + 1); + const miningClaimBlock = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user1, miaCityName, claimHeight + 1), ccd006CityMiningV2.claimMiningReward(user1, miaCityName, claimHeight - 1), ccd006CityMiningV2.claimMiningReward(user1, miaCityName, claimHeight)]); + + //console.log(`miningClaimBlock: ${JSON.stringify(miningClaimBlock, null, 2)}}`); + + // assert + miningClaimBlock.receipts[2].result.expectOk().expectBool(true); + miningClaimBlock.receipts[1].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + miningClaimBlock.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + }, +}); + +// ============================= +// 3. REWARD DELAY +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-reward-delay() can only be called by the dao", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const { receipts } = chain.mineBlock([ccd006CityMiningV2.setRewardDelay(sender, 50)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(100); + receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-reward-delay() cannot set a zero block delay", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_004); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(100); + receipts[3].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_INVALID_DELAY); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-reward-delay() successfully changes the reward delay when called by the dao", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + ccd006CityMiningV2.getRewardDelay().result.expectUint(100); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_007); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(50); + assertEquals(receipts.length, 4); + receipts[3].result.expectOk(); + }, +}); + +// ============================= +// 4. MINING STATUS +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: mine() fails if mining is disabled in the contract", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const entries: number[] = [10]; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passCcip014(chain, accounts); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); + + const block = chain.mineBlock([ccd006CityMiningV2.mine(user, miaCityName, entries)]); + + // assert + const firstBlock = block.height - 1; + const userId = 1; + + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINING_DISABLED); + + const expectedMiningStats = { + amount: types.uint(0), + claimed: types.bool(false), + miners: types.uint(0), + }; + assertEquals(ccd006CityMiningV2.getMiningStats(miaCityId, firstBlock).result.expectTuple(), expectedMiningStats); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() returns ERR_NO_MINER_DATA after mining is disabled if user did not mine in that block", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user = accounts.get("wallet_1")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + passProposal(chain, accounts, PROPOSALS.TEST_CCD003_USER_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_005); + const claimHeight = 6; // one less than actual bh + chain.mineEmptyBlock(rewardDelay); + const block = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user, miaCityName, claimHeight)]); + + // assert + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_NO_MINER_DATA); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: claim-mining-reward() succeeds after mining is disabled", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + const entries = [10]; + const totalAmount = entries.reduce((a, b) => a + b, 0); + const totalBlocks = entries.length; + + // act + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + passCcip014(chain, accounts); + + const miningBlock = chain.mineBlock([ccd006CityMiningV2.mine(user1, miaCityName, entries), ccd006CityMiningV2.mine(user2, miaCityName, entries)]); + // console.log(JSON.stringify(miningBlock, null, 2)); + const claimHeight = miningBlock.height - 1; + const lastBlock = claimHeight + totalBlocks - 1; + chain.mineEmptyBlock(rewardDelay + 1); + + // disable mining + passProposal(chain, accounts, PROPOSALS.TEST_CCD006_CITY_MINING_V2_005); + + const miningClaimUser1 = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user1, miaCityName, claimHeight)]); + const miningClaimUser2 = chain.mineBlock([ccd006CityMiningV2.claimMiningReward(user2, miaCityName, claimHeight)]); + + // assert + + const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(miaCityId).result.expectTuple(); + // verify coinbase amounts + const expectedAmounts = { + cbaBonus: types.uint(10), + cba1: types.uint(100), + cba2: types.uint(1000), + cba3: types.uint(10000), + cba4: types.uint(100000), + cba5: types.uint(1000000), + cbaDefault: types.uint(10000000), + }; + assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); + // verify coinbase thresholds + const expectedThresholds = { + cbt1: types.uint(50), + cbt2: types.uint(60), + cbt3: types.uint(70), + cbt4: types.uint(80), + cbt5: types.uint(90), + }; + assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); + + miningBlock.receipts[0].result.expectOk().expectBool(true); + miningBlock.receipts[1].result.expectOk().expectBool(true); + + let winner: number; + + if (miningClaimUser2.receipts[0].result === "(err u6014)") { + //console.log("USER 1 WINS"); + miningClaimUser1.receipts[0].result.expectOk().expectBool(true); + miningClaimUser2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + winner = 2; + } else { + //console.log("USER 2 WINS"); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); + miningClaimUser2.receipts[0].result.expectOk().expectBool(true); + winner = 3; + } + + miningBlock.receipts[0].events.expectSTXTransferEvent(10, user1.address, `${sender.address}.${miaTreasuryNameV2}`); + miningBlock.receipts[1].events.expectSTXTransferEvent(10, user2.address, `${sender.address}.${miaTreasuryNameV2}`); + // {cityId: u1, cityName: \"mia\", cityTreasury: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining, event: \"mining\", firstBlock: u10, lastBlock: u10, totalAmount: u10, totalBlocks: u1, userId: u1}"} + let expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(2)}}`; + // console.log(JSON.stringify(miningBlock.receipts[0].events), null, 2); + miningBlock.receipts[0].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + expectedPrintMsg = `{cityId: u1, cityName: "mia", cityTreasury: ${sender.address}.${miaTreasuryNameV2}, event: "mining", firstBlock: ${types.uint(claimHeight)}, lastBlock: ${types.uint(lastBlock)}, totalAmount: ${types.uint(totalAmount)}, totalBlocks: ${types.uint(totalBlocks)}, userId: ${types.uint(3)}}`; + miningBlock.receipts[1].events.expectPrintEvent(`${sender.address}.ccd006-citycoin-mining-v2`, expectedPrintMsg); + + //dumpMiningData(ccd006CityMiningV2, miaCityId, (firstBlock), (1), miningStatsAt, minerAt); + ccd006CityMiningV2.getRewardDelay().result.expectUint(rewardDelay); + + //ccd006CityMiningV2.getBlockWinner(miaCityId, firstBlock).result.expectUint(2); + ccd006CityMiningV2.getBlockWinner(miaCityId, claimHeight).result.expectSome().expectUint(winner); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: set-mining-enabled() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + // act + const block = chain.mineBlock([ccd006CityMiningV2.setMiningEnabled(sender, true)]); + // assert + block.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-mining-enabled() returns true after deployment", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + // act + passCcip014(chain, accounts); + // assert + ccd006CityMiningV2.isMiningEnabled().result.expectBool(true); + }, +}); + +// ============================= +// 5. READ-ONLY FUNCTIONS +// ============================= + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if coinbase info isn't set", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + + // act + const { result } = ccd006CityMiningV2.getCoinbaseAmount(miaCityId, 100); + + // assert + result.expectUint(0); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if the city activation details do not exist", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + // get MIA/NYC city IDs + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city status to activated + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // set city activation details + // passProposal(PROPOSALS.TEST_CCD005_CITY_DATA_004); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + + // act + const { result } = ccd006CityMiningV2.getCoinbaseAmount(miaCityId, 100); + + // assert + result.expectUint(0); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns u0 if the block height is before the city's activation height", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + // get MIA/NYC city IDs + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city status to activated + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // set city activation details + // activation block = 10 + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_004); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + + // act + const { result } = ccd006CityMiningV2.getCoinbaseAmount(miaCityId, 5); + + // assert + result.expectUint(0); + }, +}); + +Clarinet.test({ + name: "ccd006-citycoin-mining-v2: get-coinbase-amount() returns the expected amounts", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd005CityData = new CCD005CityData(chain, sender, "ccd005-city-data"); + const ccd006CityMiningV2 = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining-v2"); + // get MIA/NYC city IDs + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set city activation details + // activation block = 5 + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set city status to activated + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + // set city coinbase amounts + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_009); + // set city coinbase thresholds + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_010); + // set city coinbase details + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_018); + + // act + const coinbaseInfo = ccd005CityData.getCityCoinbaseInfo(miaCityId).result.expectTuple(); + // verify coinbase amounts + const expectedAmounts = { + cbaBonus: types.uint(10), + cba1: types.uint(100), + cba2: types.uint(1000), + cba3: types.uint(10000), + cba4: types.uint(100000), + cba5: types.uint(1000000), + cbaDefault: types.uint(10000000), + }; + + // verify coinbase thresholds + const expectedThresholds = { + cbt1: types.uint(50), + cbt2: types.uint(60), + cbt3: types.uint(70), + cbt4: types.uint(80), + cbt5: types.uint(90), + }; + + // verify coinbase details + const expectedDetails = { + bonus: types.uint(20), + epoch: types.uint(1), + }; + + // assert + + // const activation = ccd005CityData.getCityActivationDetails(miaCityId).result.expectSome().expectTuple(); + // console.log(`activation: ${JSON.stringify(activation)}`); + + // verify coinbase details + assertEquals(coinbaseInfo.amounts.expectSome().expectTuple(), expectedAmounts); + assertEquals(coinbaseInfo.thresholds.expectSome().expectTuple(), expectedThresholds); + assertEquals(coinbaseInfo.details.expectSome().expectTuple(), expectedDetails); + + // get coinbase amount based on thresholds + const thresholds = [25, 50, 60, 70, 80, 90]; + let counter = 0; + for (const threshold of thresholds) { + const { result } = ccd006CityMiningV2.getCoinbaseAmount(miaCityId, threshold); + // console.log(`result for ${threshold}[${counter}]: ${result}`); + assertEquals(result, expectedAmounts[Object.keys(expectedAmounts)[counter]]); + counter++; + } + }, +}); diff --git a/tests/extensions/ccd006-citycoin-mining.test.ts b/tests/extensions/ccd006-citycoin-mining.test.ts index 1c311a4f..930472fd 100644 --- a/tests/extensions/ccd006-citycoin-mining.test.ts +++ b/tests/extensions/ccd006-citycoin-mining.test.ts @@ -8,7 +8,7 @@ * 5. read-only functions */ import { Account, assert, assertEquals, Clarinet, Chain, types } from "../../utils/deps.ts"; -import { constructAndPassProposal, EXTENSIONS, passProposal, PROPOSALS } from "../../utils/common.ts"; +import { constructAndPassProposal, EXTENSIONS, passCcip014, passProposal, PROPOSALS } from "../../utils/common.ts"; import { CCD002Treasury } from "../../models/extensions/ccd002-treasury.model.ts"; import { CCD003UserRegistry } from "../../models/extensions/ccd003-user-registry.model.ts"; import { CCD005CityData } from "../../models/extensions/ccd005-city-data.model.ts"; @@ -1049,7 +1049,7 @@ Clarinet.test({ block1.receipts[0].result.expectOk().expectBool(true); block1.receipts[1].result.expectOk().expectBool(true); - if (block2.receipts[0].result === "(err u6011)") { + if (block2.receipts[0].result === "(err u6015)") { //console.log("USER 2 WINS"); block2.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); block2.receipts[1].result.expectErr().expectUint(CCD010CoreV2Adapter.ErrCode.ERR_NOTHING_TO_MINT); @@ -1153,7 +1153,7 @@ Clarinet.test({ winner = 1; } else { //console.log("USER 2 WINS"); - miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); miningClaimUser2.receipts[0].result.expectOk().expectBool(true); winner = 2; } @@ -1447,7 +1447,7 @@ Clarinet.test({ winner = 1; } else { //console.log("USER 2 WINS"); - miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_ALREADY_CLAIMED); + miningClaimUser1.receipts[0].result.expectErr().expectUint(CCD006CityMining.ErrCode.ERR_MINER_NOT_WINNER); miningClaimUser2.receipts[0].result.expectOk().expectBool(true); winner = 2; } diff --git a/utils/common.ts b/utils/common.ts index 11e1ba17..ab1c803e 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -1,6 +1,9 @@ import { Account, Chain } from "./deps.ts"; -import { CCD001DirectExecute } from "../models/extensions/ccd001-direct-execute.model.ts"; import { BaseDao } from "../models/base-dao.model.ts"; +import { CCD001DirectExecute } from "../models/extensions/ccd001-direct-execute.model.ts"; +import { CCD006CityMining } from "../models/extensions/ccd006-citycoin-mining.model.ts"; +import { CCD007CityStacking } from "../models/extensions/ccd007-citycoin-stacking.model.ts"; +import { CCIP014Pox3 } from "../models/proposals/ccip014-pox-3.model.ts"; // Toggle startBlock to align starting block height - this varies with contracts (and nested contracts) in toml // export const START_BLOCK_BASE_DAO = 100; // or 99 @@ -15,8 +18,10 @@ export const BASE_DAO = ADDRESS.concat(".base-dao"); export const EXTENSIONS = { CCD001_DIRECT_EXECUTE: ADDRESS.concat(".ccd001-direct-execute"), CCD002_TREASURY_MIA_MINING: ADDRESS.concat(".ccd002-treasury-mia-mining"), + CCD002_TREASURY_MIA_MINING_V2: ADDRESS.concat(".ccd002-treasury-mia-mining-v2"), CCD002_TREASURY_MIA_STACKING: ADDRESS.concat(".ccd002-treasury-mia-stacking"), CCD002_TREASURY_NYC_MINING: ADDRESS.concat(".ccd002-treasury-nyc-mining"), + CCD002_TREASURY_NYC_MINING_V2: ADDRESS.concat(".ccd002-treasury-nyc-mining-v2"), CCD002_TREASURY_NYC_STACKING: ADDRESS.concat(".ccd002-treasury-nyc-stacking"), CCD003_USER_REGISTRY: ADDRESS.concat(".ccd003-user-registry"), CCD004_CITY_REGISTRY: ADDRESS.concat(".ccd004-city-registry"), @@ -73,6 +78,7 @@ export const PROPOSALS = { TEST_CCD005_CITY_DATA_017: ADDRESS.concat(".test-ccd005-city-data-017"), TEST_CCD005_CITY_DATA_018: ADDRESS.concat(".test-ccd005-city-data-018"), TEST_CCD005_CITY_DATA_019: ADDRESS.concat(".test-ccd005-city-data-019"), + TEST_CCD005_CITY_DATA_020: ADDRESS.concat(".test-ccd005-city-data-020"), TEST_CCD006_CITY_MINING_001: ADDRESS.concat(".test-ccd006-citycoin-mining-001"), TEST_CCD006_CITY_MINING_002: ADDRESS.concat(".test-ccd006-citycoin-mining-002"), TEST_CCD006_CITY_MINING_003: ADDRESS.concat(".test-ccd006-citycoin-mining-003"), @@ -83,6 +89,8 @@ export const PROPOSALS = { TEST_CCD006_CITY_MINING_V2_003: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-003"), TEST_CCD006_CITY_MINING_V2_004: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-004"), TEST_CCD006_CITY_MINING_V2_005: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-005"), + TEST_CCD006_CITY_MINING_V2_006: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-006"), + TEST_CCD006_CITY_MINING_V2_007: ADDRESS.concat(".test-ccd006-citycoin-mining-v2-007"), TEST_CCD007_CITY_STACKING_001: ADDRESS.concat(".test-ccd007-citycoin-stacking-001"), TEST_CCD007_CITY_STACKING_002: ADDRESS.concat(".test-ccd007-citycoin-stacking-002"), TEST_CCD007_CITY_STACKING_003: ADDRESS.concat(".test-ccd007-citycoin-stacking-003"), @@ -136,6 +144,48 @@ export const constructAndPassProposal = (chain: Chain, accounts: Map<string, Acc return block; }; +export const passCcip014 = (chain: Chain, accounts: Map<string, Account>): any => { + // 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 ccip014pox3 = new CCIP014Pox3(chain, sender); + + const miningEntries = [25000000, 25000000]; + const amountStacked = 500; + const lockPeriod = 10; + + // act + // progress the chain to avoid underflow in + // stacking reward cycle calculation + chain.mineEmptyBlockUntil(CCD007CityStacking.FIRST_STACKING_BLOCK); + + // prepare for CCIP-014 + 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([ccip014pox3.voteOnProposal(user1, true)]); + + // upgrade to v2 + return passProposal(chain, accounts, PROPOSALS.CCIP_014_V2); +}; + // reusable city data export type CityData = { From 478dc4821d00054546ed1dc28647e15b3b871b17 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:37:05 -0700 Subject: [PATCH 46/47] fix: add ccd002-treasury-v2 tests --- Clarinet.toml | 36 + .../test-ccd002-treasury-v2-001.clar | 16 + .../test-ccd002-treasury-v2-002.clar | 26 + .../test-ccd002-treasury-v2-003.clar | 19 + .../test-ccd002-treasury-v2-004.clar | 23 + .../test-ccd002-treasury-v2-005.clar | 20 + .../test-ccd002-treasury-v2-006.clar | 14 + .../test-ccd002-treasury-v2-007.clar | 14 + .../test-ccd002-treasury-v2-008.clar | 22 + .../test-ccd002-treasury-v2-009.clar | 15 + .../test-ccd002-treasury-v2-010.clar | 14 + .../test-ccd002-treasury-v2-011.clar | 19 + .../test-ccd002-treasury-v2-012.clar | 14 + tests/extensions/ccd002-treasury-v2.test.ts | 701 ++++++++++++++++++ utils/common.ts | 12 + 15 files changed, 965 insertions(+) create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-001.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-002.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-003.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-004.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-005.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-006.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-007.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-008.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-009.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-010.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-011.clar create mode 100644 tests/contracts/proposals/test-ccd002-treasury-v2-012.clar create mode 100644 tests/extensions/ccd002-treasury-v2.test.ts diff --git a/Clarinet.toml b/Clarinet.toml index 244f32fc..9d1ea6ee 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -271,6 +271,42 @@ path = "tests/contracts/proposals/test-ccd002-treasury-011.clar" [contracts.test-ccd002-treasury-012] path = "tests/contracts/proposals/test-ccd002-treasury-012.clar" +[contracts.test-ccd002-treasury-v2-001] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-001.clar" + +[contracts.test-ccd002-treasury-v2-002] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-002.clar" + +[contracts.test-ccd002-treasury-v2-003] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-003.clar" + +[contracts.test-ccd002-treasury-v2-004] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-004.clar" + +[contracts.test-ccd002-treasury-v2-005] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-005.clar" + +[contracts.test-ccd002-treasury-v2-006] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-006.clar" + +[contracts.test-ccd002-treasury-v2-007] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-007.clar" + +[contracts.test-ccd002-treasury-v2-008] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-008.clar" + +[contracts.test-ccd002-treasury-v2-009] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-009.clar" + +[contracts.test-ccd002-treasury-v2-010] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-010.clar" + +[contracts.test-ccd002-treasury-v2-011] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-011.clar" + +[contracts.test-ccd002-treasury-v2-012] +path = "tests/contracts/proposals/test-ccd002-treasury-v2-012.clar" + [contracts.test-ccd003-user-registry-001] path = "tests/contracts/proposals/test-ccd003-user-registry-001.clar" diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-001.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-001.clar new file mode 100644 index 00000000..e44429a7 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-001.clar @@ -0,0 +1,16 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: set-allowed() succeeds and adds a contract principal +;; ccd002-treasury: is-allowed() succeeds and returns true if asset is found in map +;; ccd002-treasury: get-allowed-asset() succeeds and returns tuple if asset is found in map + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-mia true)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-002.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-002.clar new file mode 100644 index 00000000..d5f094a7 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-002.clar @@ -0,0 +1,26 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: set-allowed-list() succeeds and adds contract principals +;; ccd002-treasury: set-allowed-list() succeeds and toggles the state of the asset contracts +;; ccd002-treasury: deposit-ft() fails if asset is not allowed +;; ccd002-treasury: deposit-ft() succeeds and transfers FT to the vault +;; ccd002-treasury: deposit-nft() fails if asset is not allowed +;; ccd002-treasury: withdraw-ft() fails if asset is not allowed +;; ccd002-treasury: withdraw-nft() fails if asset is not allowed + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed-list + (list + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-mia, enabled: true} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc, enabled: false} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-nft-nyc, enabled: false} + ) + )) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-003.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-003.clar new file mode 100644 index 00000000..77afcef3 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-003.clar @@ -0,0 +1,19 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: set-allowed-list() succeeds and toggles the state of the asset contracts + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed-list + (list + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-mia, enabled: false} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-nyc, enabled: true} + ) + )) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-004.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-004.clar new file mode 100644 index 00000000..35f00b09 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-004.clar @@ -0,0 +1,23 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: deposit-ft() succeeds and transfers FT to the vault + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; mint type 01 tokens to wallets 5,6,7,8 for testing + (try! (contract-call? .test-ccext-governance-token-mia edg-mint u2000 'ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB)) + (try! (contract-call? .test-ccext-governance-token-mia edg-mint u2000 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (try! (contract-call? .test-ccext-governance-token-mia edg-mint u2000 'ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ)) + (try! (contract-call? .test-ccext-governance-token-mia edg-mint u2000 'ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP)) + ;; mint type 02 tokens to wallets 5,6,7,8 for testing + (try! (contract-call? .test-ccext-governance-token-nyc edg-mint u2000 'ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB)) + (try! (contract-call? .test-ccext-governance-token-nyc edg-mint u2000 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (try! (contract-call? .test-ccext-governance-token-nyc edg-mint u2000 'ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ)) + (try! (contract-call? .test-ccext-governance-token-nyc edg-mint u2000 'ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-005.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-005.clar new file mode 100644 index 00000000..3ba267fe --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-005.clar @@ -0,0 +1,20 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: deposit-nft() succeeds and transfers NFT to the vault +;; ccd002-treasury: withdraw-nft() succeeds and transfers NFT to recipient + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 set-allowed-list + (list + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-nft-mia, enabled: true} + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-nft-nyc, enabled: true} + ) + )) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-006.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-006.clar new file mode 100644 index 00000000..aaf7aed1 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-006.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: withdraw-stx() succeeds and transfers STX to recipient + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-mia-mining-v2 withdraw-stx u500 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-007.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-007.clar new file mode 100644 index 00000000..d988c886 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-007.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: withdraw-ft() fails if asset is not allowed + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-ft .test-ccext-governance-token-nyc u500 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-008.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-008.clar new file mode 100644 index 00000000..45e2a4fd --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-008.clar @@ -0,0 +1,22 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: withdraw-ft() fails if withdrawal exceed balance +;; ccd002-treasury: withdraw-ft() succeeds and transfers FT to recipient + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; tests success of setting sunset height + (try! (contract-call? .ccd002-treasury-mia-mining-v2 set-allowed-list + (list + {token: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-mia, enabled: true} + ) + )) + (try! (contract-call? .test-ccext-governance-token-mia edg-mint u2000 .ccd002-treasury-mia-mining-v2)) + (try! (contract-call? .ccd002-treasury-mia-mining-v2 withdraw-ft .test-ccext-governance-token-mia u500 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-009.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-009.clar new file mode 100644 index 00000000..b5276782 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-009.clar @@ -0,0 +1,15 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: withdraw-nft() fails if asset is not allowed +;; ccd002-treasury: withdraw-nft() succeeds and transfers NFT to recipient + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 withdraw-nft 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-nft-nyc u1 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-010.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-010.clar new file mode 100644 index 00000000..d0005334 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-010.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: withdraw-ft() fails if withdrawal exceed balance + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-mia-mining-v2 withdraw-ft .test-ccext-governance-token-mia u2000 'ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-011.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-011.clar new file mode 100644 index 00000000..b3481b93 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-011.clar @@ -0,0 +1,19 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: stack-stx() succeeds and delegates STX + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + ;; revoke delegation from ccip012-bootstrap + (try! (contract-call? .ccd002-treasury-mia-mining-v2 revoke-delegate-stx)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 revoke-delegate-stx)) + ;; delegate all treasuries in Clarinet.toml + (try! (contract-call? .ccd002-treasury-mia-mining-v2 delegate-stx u1000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) + (try! (contract-call? .ccd002-treasury-nyc-mining-v2 delegate-stx u1000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) + (ok true) + ) +) diff --git a/tests/contracts/proposals/test-ccd002-treasury-v2-012.clar b/tests/contracts/proposals/test-ccd002-treasury-v2-012.clar new file mode 100644 index 00000000..18e961d4 --- /dev/null +++ b/tests/contracts/proposals/test-ccd002-treasury-v2-012.clar @@ -0,0 +1,14 @@ +;; Title: Test Proposal +;; Version: 1.0.0 +;; Synopsis: Test proposal for clarinet layer +;; Description: +;; ccd002-treasury: revoke-delegate-stx() succeeds and revokes stacking delegation + +(impl-trait .proposal-trait.proposal-trait) + +(define-public (execute (sender principal)) + (begin + (try! (contract-call? .ccd002-treasury-mia-mining-v2 revoke-delegate-stx)) + (ok true) + ) +) diff --git a/tests/extensions/ccd002-treasury-v2.test.ts b/tests/extensions/ccd002-treasury-v2.test.ts new file mode 100644 index 00000000..7acf941e --- /dev/null +++ b/tests/extensions/ccd002-treasury-v2.test.ts @@ -0,0 +1,701 @@ +import { Account, assertEquals, Clarinet, Chain } from "../../utils/deps.ts"; +import { constructAndPassProposal, passProposal, EXTENSIONS, EXTERNAL, PROPOSALS } from "../../utils/common.ts"; +import { CCD002Treasury, PoxAddress } from "../../models/extensions/ccd002-treasury.model.ts"; +import { CCEXTGovernanceToken } from "../../models/external/test-ccext-governance-token.model.ts"; +import { CCEXTNft } from "../../models/external/test-ccext-nft.model.ts"; + +// PUBLIC FUNCTIONS + +Clarinet.test({ + name: "ccd002-treasury: is-dao-or-extension() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + + // assert + ccd002Treasury.isDaoOrExtension().result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: callback() succeeds when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.callback(sender, "test")]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectOk().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: set-allowed() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + + // act + const { receipts } = chain.mineBlock([ + ccd002Treasury.setAllowed(sender, { + token: EXTERNAL.FT_MIA, + enabled: true, + }), + ]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: set-allowed() succeeds and adds a contract principal", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_001); + + // assert + assertEquals(receipts.length, 4); + receipts[0].result.expectOk().expectBool(true); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: set-allowed-list() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const assetList = [ + { + token: EXTERNAL.FT_MIA, + enabled: true, + }, + { + token: EXTERNAL.FT_NYC, + enabled: true, + }, + ]; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.setAllowedList(sender, assetList)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: set-allowed-list() succeeds and adds contract principals", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_MIA).result.expectNone(); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_NYC).result.expectNone(); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + + // assert + assertEquals(receipts.length, 4); + receipts[0].result.expectOk().expectBool(true); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + ccd002Treasury.isAllowed(EXTERNAL.FT_NYC).result.expectBool(false); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_MIA).result.expectSome().expectBool(true); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_NYC).result.expectSome().expectBool(false); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: set-allowed-list() succeeds and toggles the state of the asset contracts", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_MIA).result.expectNone(); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_NYC).result.expectNone(); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + ccd002Treasury.isAllowed(EXTERNAL.FT_NYC).result.expectBool(false); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_MIA).result.expectSome().expectBool(true); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_NYC).result.expectSome().expectBool(false); + + // act + // prop 3 toggles the state of a list of assets + passProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_003); + + // assert + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(false); + ccd002Treasury.isAllowed(EXTERNAL.FT_NYC).result.expectBool(true); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_MIA).result.expectSome().expectBool(false); + ccd002Treasury.getAllowedAsset(EXTERNAL.FT_NYC).result.expectSome().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-stx() succeeds and transfers STX to the vault", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount = 1000; + const event = '{amount: u1000, caller: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM, event: "deposit-stx", recipient: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining-v2, sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM}'; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.depositStx(sender, amount)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectOk().expectBool(true); + receipts[0].events.expectSTXTransferEvent(amount, sender.address, EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2); + receipts[0].events.expectPrintEvent(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2, event); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-ft() fails if asset is not on allow list", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount = 1000; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.depositFt(sender, EXTERNAL.FT_MIA, amount)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-ft() fails if asset is not enabled", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount = 1000; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + ccd002Treasury.isAllowed(EXTERNAL.FT_NYC).result.expectBool(false); + + // act + const block = chain.mineBlock([ccd002Treasury.depositFt(sender, EXTERNAL.FT_NYC, amount)]); + + // assert + assertEquals(block.receipts.length, 1); + block.receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-ft() succeeds and transfers FT to the vault", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const depositor = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + const amount = 1000; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + // TEST_CCD002_TREASURY_004 mints 2000 MIA to wallet 6 + passProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_004); + gt.getBalance(depositor.address).result.expectOk().expectUint(2000); + const event = '{amount: u1000, assetContract: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccext-governance-token-mia, caller: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0, event: "deposit-ft", recipient: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-mia-mining-v2, sender: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0}'; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.depositFt(depositor, EXTERNAL.FT_MIA, amount)]); + + // assert + assertEquals(receipts.length, 1); + gt.getBalance(depositor.address).result.expectOk().expectUint(1000); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(1000); + receipts[0].result.expectOk().expectBool(true); + receipts[0].events.expectFungibleTokenTransferEvent(1000, depositor.address, EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2, EXTERNAL.FT_MIA + "::miamicoin"); + receipts[0].events.expectPrintEvent(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2, event); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-nft() fails if asset is not on allow list", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const tokenId = 1000; + ccd002Treasury.isAllowed(EXTERNAL.NFT_NYC).result.expectBool(false); + + // act + const block = chain.mineBlock([ccd002Treasury.depositNft(sender, EXTERNAL.NFT_NYC, tokenId)]); + + // assert + assertEquals(block.receipts.length, 1); + block.receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-nft() fails if asset is not enabled", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const tokenId = 1000; + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + ccd002Treasury.isAllowed(EXTERNAL.NFT_NYC).result.expectBool(false); + + // act + const block = chain.mineBlock([ccd002Treasury.depositNft(sender, EXTERNAL.NFT_NYC, tokenId)]); + + // assert + assertEquals(block.receipts.length, 1); + block.receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: deposit-nft() succeeds and transfers NFT to the vault", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const depositor = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + const nft = new CCEXTNft(chain, sender, "test-ccext-nft-nyc"); + const tokenId = 1; + // Enable NYC NFT asset contract + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_005); + ccd002Treasury.isAllowed(EXTERNAL.NFT_NYC).result.expectBool(true); + chain.mineBlock([nft.mint(depositor.address, sender.address)]); + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(depositor.address); + const event = "{assetContract: " + EXTERNAL.NFT_NYC + ', caller: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0, event: "deposit-nft", recipient: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ccd002-treasury-nyc-mining-v2, sender: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0, tokenId: u1}'; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.depositNft(depositor, EXTERNAL.NFT_NYC, tokenId)]); + + // assert + assertEquals(receipts.length, 1); + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2); + receipts[0].result.expectOk().expectBool(true); + receipts[0].events.expectNonFungibleTokenTransferEvent("u1", depositor.address, EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, EXTERNAL.NFT_NYC, "nyc"); + receipts[0].events.expectPrintEvent(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, event); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-stx() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount = 1000; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.withdrawStx(sender, amount, sender.address)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-stx() succeeds and transfers STX to recipient", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const recipient = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount1 = 1000; + const amount2 = 500; + chain.mineBlock([ccd002Treasury.depositStx(sender, amount1)]); + ccd002Treasury.getBalanceStx().result.expectUint(amount1); + const event = '{amount: u500, caller: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccd002-treasury-v2-006, event: "withdraw-stx", recipient: ' + recipient.address + ", sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.base-dao}"; + + // act + // TEST_CCD002_TREASURY_006 calls the withdraw-stx of 500 stx to wallet_6 + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_006); + + // assert + ccd002Treasury.getBalanceStx().result.expectUint(amount2); + assertEquals(receipts.length, 4); + receipts[3].result.expectOk().expectUint(3); // number of signals - not result of withdraw-stx! + receipts[3].events.expectSTXTransferEvent(amount2, EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2, recipient.address); + receipts[3].events.expectPrintEvent(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2, event); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-ft() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const recipient = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + const amount = 1000; + + // act + const block = chain.mineBlock([ccd002Treasury.withdrawFt(sender, EXTERNAL.FT_NYC, amount, recipient.address)]); + + // assert + assertEquals(block.receipts.length, 1); + block.receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-ft() fails if asset is not on allow list", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + ccd002Treasury.isAllowed(EXTERNAL.FT_NYC).result.expectBool(false); + + // act + // Proposal attempts to withdraw nyc coin + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_007); + + // assert + assertEquals(receipts.length, 4); + receipts[3].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-ft() fails if asset is not enabled", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + ccd002Treasury.isAllowed(EXTERNAL.FT_NYC).result.expectBool(false); + + // act + // Proposal attempts to withdraw nyc coin + const { receipts } = passProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_007); + + // assert + assertEquals(receipts.length, 3); + receipts[2].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-ft() fails if withdrawal exceeds balance", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const recipient = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + gt.getBalance(recipient.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(0); + // Prop 8 allow lists MIA token, mints 2000 MIA to treasury and withdraws 500 to recipient + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_008); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + gt.getBalance(recipient.address).result.expectOk().expectUint(500); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(1500); + + // act + // Prop 10 transfers 2000 mia tokens to recipient + const { receipts } = passProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_010); + + // assert + // check balances have not changed + gt.getBalance(recipient.address).result.expectOk().expectUint(500); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(1500); + assertEquals(receipts.length, 3); + receipts[2].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_NOT_ENOUGH_FUNDS); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-ft() succeeds and transfers FT to recipient", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const recipient = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const gt = new CCEXTGovernanceToken(chain, sender, "test-ccext-governance-token-mia"); + gt.getBalance(recipient.address).result.expectOk().expectUint(0); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(0); + + // act + // Prop 8 allow lists MIA token, mints 2000 MIA to treasury and withdraws 500 to recipient + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_008); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + + // assert + assertEquals(receipts.length, 4); + receipts[3].result.expectOk().expectUint(3); + // check the balances correspond to the proposal. + gt.getBalance(recipient.address).result.expectOk().expectUint(500); + gt.getBalance(EXTENSIONS.CCD002_TREASURY_MIA_MINING_V2).result.expectOk().expectUint(1500); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-nft() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const recipient = accounts.get("wallet_6")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const tokenId = 1; + + // act + const { receipts } = chain.mineBlock([ccd002Treasury.withdrawNft(sender, EXTERNAL.NFT_MIA, tokenId, recipient.address)]); + + // assert + assertEquals(receipts.length, 1); + receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-nft() fails if asset is not on allow list", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + ccd002Treasury.isAllowed(EXTERNAL.NFT_NYC).result.expectBool(false); + const tokenId = 1; + const nft = new CCEXTNft(chain, sender, "test-ccext-nft-nyc"); + // mint an asset to the treasury + chain.mineBlock([nft.mint(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, sender.address)]); + // Check asset is owned by the nyc treasury + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2); + + // act + // Proposal 9 attempts to withdraw nyc nft + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_009); + + // assert + // Demonstrate ownership of asset is unchanged + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2); + assertEquals(receipts.length, 4); + receipts[3].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-nft() fails if asset is not enabled", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_002); + ccd002Treasury.isAllowed(EXTERNAL.NFT_NYC).result.expectBool(false); + const tokenId = 1; + const nft = new CCEXTNft(chain, sender, "test-ccext-nft-nyc"); + // mint an asset to the treasury + chain.mineBlock([nft.mint(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, sender.address)]); + // Check asset is owned by the nyc treasury + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2); + + // act + // Proposal 9 attempts to withdraw nyc nft + const { receipts } = passProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_009); + + // assert + // Demonstrate ownership of asset is unchanged + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2); + assertEquals(receipts.length, 3); + receipts[2].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNKNOWN_ASSET); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: withdraw-nft() succeeds and transfers NFT to recipient", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-nyc-mining-v2"); + const recipient = accounts.get("wallet_6")!; + const nft = new CCEXTNft(chain, sender, "test-ccext-nft-nyc"); + const tokenId = 1; + // Proposal 5 allow lists NYC NFT asset contract + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_005); + ccd002Treasury.isAllowed(EXTERNAL.NFT_NYC).result.expectBool(true); + // mint an asset to the treasury + chain.mineBlock([nft.mint(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, sender.address)]); + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2); + const event = "{assetContract: " + EXTERNAL.NFT_NYC + ', caller: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-ccd002-treasury-v2-009, event: "withdraw-nft", recipient: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0, sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.base-dao, tokenId: u1}'; + + // act + // proposal 9 transfers the nft from the treasury to recipient + const { receipts } = passProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_009); + + // assert + receipts[2].result.expectOk().expectUint(3); + // check ownership of asset has changed + nft.getOwner(tokenId).result.expectOk().expectSome().expectPrincipal(recipient.address); + assertEquals(receipts.length, 3); + receipts[2].events.expectNonFungibleTokenTransferEvent("u1", EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, recipient.address, EXTERNAL.NFT_NYC, "nyc"); + receipts[2].events.expectPrintEvent(EXTENSIONS.CCD002_TREASURY_NYC_MINING_V2, event); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: delegate-stx() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount = 1000000000; // 1,000 STX + const delegateStx = accounts.get("wallet_1")!; + + // act + const block = chain.mineBlock([ccd002Treasury.delegateStx(sender, amount, delegateStx.address)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: delegate-stx() succeeds and delegates STX when called by a proposal", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_011); + + // assert + assertEquals(receipts.length, 4); + receipts[0].result.expectOk().expectBool(true); + receipts[1].result.expectOk().expectUint(1); + receipts[2].result.expectOk().expectUint(2); + receipts[3].result.expectOk().expectUint(3); + // TODO: match print event + // TODO: add get-stacker-info from pox? + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: revoke-delegate-stx() fails when called directly", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")! as Account; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + + // act + const block = chain.mineBlock([ccd002Treasury.revokeDelegateStx(sender)]); + + // assert + block.receipts[0].result.expectErr().expectUint(CCD002Treasury.ErrCode.ERR_UNAUTHORIZED); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: revoke-delegate-stx() succeeds and revokes delegatation when called by a proposal", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_012); + + // assert + assertEquals(receipts.length, 4); + receipts[0].result.expectOk().expectBool(true); + receipts[1].result.expectOk().expectUint(1); + receipts[2].result.expectOk().expectUint(2); + receipts[3].result.expectOk().expectUint(3); + // TODO: match print event + // TODO: add get-stacker-info from pox? + }, +}); + +// READ ONLY FUNCTIONS + +Clarinet.test({ + name: "ccd002-treasury: is-allowed() succeeds and returns false if asset is not in map", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const asset = EXTERNAL.FT_MIA; + + // assert + ccd002Treasury.isAllowed(asset).result.expectBool(false); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: is-allowed() succeeds and returns true if asset is found in map", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(false); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_001); + + // assert + assertEquals(receipts.length, 4); + receipts[0].result.expectOk().expectBool(true); + ccd002Treasury.isAllowed(EXTERNAL.FT_MIA).result.expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: get-allowed-asset() succeeds and returns none if asset is not in map", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const asset = EXTERNAL.FT_MIA; + + // assert + ccd002Treasury.getAllowedAsset(asset).result.expectNone(); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: get-allowed-asset() succeeds and returns tuple if asset is found in map", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const asset = EXTERNAL.FT_MIA; + ccd002Treasury.isAllowed(asset).result.expectBool(false); + + // act + const { receipts } = constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD002_TREASURY_V2_001); + + // assert + assertEquals(receipts.length, 4); + receipts[0].result.expectOk().expectBool(true); + ccd002Treasury.isAllowed(asset).result.expectBool(true); + ccd002Treasury.getAllowedAsset(asset).result.expectSome().expectBool(true); + }, +}); + +Clarinet.test({ + name: "ccd002-treasury: get-balance-stx() succeeds and returns STX balance of the vault", + fn(chain: Chain, accounts: Map<string, Account>) { + // arrange + const sender = accounts.get("deployer")!; + const ccd002Treasury = new CCD002Treasury(chain, sender, "ccd002-treasury-mia-mining-v2"); + const amount = 1000; + chain.mineBlock([ccd002Treasury.depositStx(sender, amount)]); + + // assert + ccd002Treasury.getBalanceStx().result.expectUint(amount); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index ab1c803e..cf496c12 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -54,6 +54,18 @@ export const PROPOSALS = { TEST_CCD002_TREASURY_010: ADDRESS.concat(".test-ccd002-treasury-010"), TEST_CCD002_TREASURY_011: ADDRESS.concat(".test-ccd002-treasury-011"), TEST_CCD002_TREASURY_012: ADDRESS.concat(".test-ccd002-treasury-012"), + TEST_CCD002_TREASURY_V2_001: ADDRESS.concat(".test-ccd002-treasury-v2-001"), + TEST_CCD002_TREASURY_V2_002: ADDRESS.concat(".test-ccd002-treasury-v2-002"), + TEST_CCD002_TREASURY_V2_003: ADDRESS.concat(".test-ccd002-treasury-v2-003"), + TEST_CCD002_TREASURY_V2_004: ADDRESS.concat(".test-ccd002-treasury-v2-004"), + TEST_CCD002_TREASURY_V2_005: ADDRESS.concat(".test-ccd002-treasury-v2-005"), + TEST_CCD002_TREASURY_V2_006: ADDRESS.concat(".test-ccd002-treasury-v2-006"), + TEST_CCD002_TREASURY_V2_007: ADDRESS.concat(".test-ccd002-treasury-v2-007"), + TEST_CCD002_TREASURY_V2_008: ADDRESS.concat(".test-ccd002-treasury-v2-008"), + TEST_CCD002_TREASURY_V2_009: ADDRESS.concat(".test-ccd002-treasury-v2-009"), + TEST_CCD002_TREASURY_V2_010: ADDRESS.concat(".test-ccd002-treasury-v2-010"), + TEST_CCD002_TREASURY_V2_011: ADDRESS.concat(".test-ccd002-treasury-v2-011"), + TEST_CCD002_TREASURY_V2_012: ADDRESS.concat(".test-ccd002-treasury-v2-012"), TEST_CCD003_USER_REGISTRY_001: ADDRESS.concat(".test-ccd003-user-registry-001"), TEST_CCD003_USER_REGISTRY_002: ADDRESS.concat(".test-ccd003-user-registry-002"), TEST_CCD003_USER_REGISTRY_003: ADDRESS.concat(".test-ccd003-user-registry-003"), From da0d48827f1df437dcba4201721bb2cc281c3990 Mon Sep 17 00:00:00 2001 From: Jason Schrader <whoabuddy@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:55:48 -0700 Subject: [PATCH 47/47] fix: add read-only tests to ccip014-pox-3 --- tests/proposals/ccip014-pox-3.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/proposals/ccip014-pox-3.test.ts b/tests/proposals/ccip014-pox-3.test.ts index 0baf635f..02576077 100644 --- a/tests/proposals/ccip014-pox-3.test.ts +++ b/tests/proposals/ccip014-pox-3.test.ts @@ -151,11 +151,35 @@ Clarinet.test({ console.log(ccip014pox3.getNycVote(nyc.cityId, userId, true)); */ + // check vote is active + ccip014pox3.isVoteActive().result.expectSome().expectBool(true); + // check proposal info + const proposalInfo = { + hash: types.ascii("0448a33745e8f157214e3da87c512a2cd382dcd2"), + link: types.ascii("https://github.com/Rapha-btc/governance/blob/patch-1/ccips/ccip-014/ccip-014-upgrade-to-pox3.md"), + name: types.ascii("Upgrade to pox-3"), + }; + assertEquals(ccip014pox3.getProposalInfo().result.expectSome().expectTuple(), proposalInfo); + // check vote period is not set (end unknown) + ccip014pox3.getVotePeriod().result.expectNone(); + // execute ccip-014 const block = passProposal(chain, accounts, PROPOSALS.CCIP_014); // 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(ccip014pox3.getVotePeriod().result.expectSome().expectTuple(), votingPeriod); + // check vote is no longer active + ccip014pox3.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); }, });