diff --git a/Clarinet.toml b/Clarinet.toml index e579de0b..9d1ea6ee 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" @@ -110,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" @@ -134,6 +143,16 @@ path = "contracts/proposals/ccip013-migration.clar" [contracts.ccip013-activation] path = "contracts/proposals/ccip013-activation.clar" +[contracts.ccip014-pox-3] +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] @@ -154,6 +173,9 @@ path = "contracts/traits/ccd006-trait.clar" [contracts.ccd007-trait] path = "contracts/traits/ccd007-trait.clar" +[contracts.ccip-015-trait] +path = "contracts/traits/ccip-015-trait.clar" + # CITYCOINS EXTERNAL CONTRACTS [contracts.citycoin-vrf-v2] @@ -171,6 +193,11 @@ 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" +clarity_version = 2 +epoch = 2.4 + # CITYCOINS LEGACY CONTRACTS [contracts.citycoin-core-v2-trait] @@ -244,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" @@ -316,6 +379,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" @@ -331,6 +397,27 @@ 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-v2-001.clar" + +[contracts.test-ccd006-citycoin-mining-v2-002] +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-v2-003.clar" + +[contracts.test-ccd006-citycoin-mining-v2-004] +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-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" @@ -370,15 +457,23 @@ 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" + +[contracts.test-ccip014-pox-3-002] +path = "tests/contracts/proposals/test-ccip014-pox-3-002.clar" + [repl] 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 new file mode 100644 index 00000000..f3475a81 --- /dev/null +++ b/contracts/extensions/ccd002-treasury-v2.clar @@ -0,0 +1,206 @@ +;; 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 ) (amount uint)) + (begin + (asserts! (is-allowed (contract-of ft)) ERR_UNKNOWN_ASSSET) + (print { + event: "deposit-ft", + amount: amount, + assetContract: (contract-of ft), + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender + }) + (contract-call? ft transfer amount tx-sender TREASURY none) + ) +) + +(define-public (deposit-nft (nft ) (id uint)) + (begin + (asserts! (is-allowed (contract-of nft)) ERR_UNKNOWN_ASSSET) + (print { + event: "deposit-nft", + assetContract: (contract-of nft), + caller: contract-caller, + recipient: TREASURY, + sender: tx-sender, + tokenId: id, + }) + (contract-call? nft transfer id tx-sender TREASURY) + ) +) + +(define-public (withdraw-stx (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "withdraw-stx", + amount: amount, + caller: contract-caller, + recipient: recipient, + sender: tx-sender + }) + (as-contract (stx-transfer? amount TREASURY recipient)) + ) +) + +(define-public (withdraw-ft (ft ) (amount uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (is-allowed (contract-of ft)) ERR_UNKNOWN_ASSSET) + (print { + event: "withdraw-ft", + assetContract: (contract-of ft), + caller: contract-caller, + recipient: recipient, + sender: tx-sender + }) + (as-contract (contract-call? ft transfer amount TREASURY recipient none)) + ) +) + +(define-public (withdraw-nft (nft ) (id uint) (recipient principal)) + (begin + (try! (is-dao-or-extension)) + (asserts! (is-allowed (contract-of nft)) ERR_UNKNOWN_ASSSET) + (print { + event: "withdraw-nft", + assetContract: (contract-of nft), + caller: contract-caller, + recipient: recipient, + sender: tx-sender, + tokenId: id + }) + (as-contract (contract-call? nft transfer id TREASURY recipient)) + ) +) + +(define-public (delegate-stx (maxAmount uint) (to principal)) + (begin + (try! (is-dao-or-extension)) + (print { + event: "delegate-stx", + amount: maxAmount, + caller: contract-caller, + delegate: to, + sender: tx-sender + }) + ;; MAINNET: 'SP000000000000000000002Q6VF78.pox-3 + ;; TESTNET: 'ST000000000000000000002AMW42H.pox-3 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 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-3 + ;; TESTNET: 'ST000000000000000000002AMW42H.pox-3 + (match (as-contract (contract-call? 'ST000000000000000000002AMW42H.pox-3 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..10bc7dfc --- /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 false) +(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-v2")) + (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/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-v2.clar b/contracts/proposals/ccip014-pox-3-v2.clar new file mode 100644 index 00000000..f4e55de5 --- /dev/null +++ b/contracts/proposals/ccip014-pox-3-v2.clar @@ -0,0 +1,73 @@ +;; TRAITS + +(impl-trait .proposal-trait.proposal-trait) + +;; ERRORS + +(define-constant ERR_PANIC (err u1400)) +(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 + +;; supplements CCIP-014 and removes code that fails before cycle 60 starts +(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 in CCIP-014 + (try! (is-executable)) + + ;; enable mining v2 contracts 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: 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)) + + ;; 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 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 Friedger pool + ;; MAINNET: SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP + (try! (contract-call? .ccd011-stacking-payouts set-pool-operator 'ST1XQXW9JNQ1W4A7PYTN3HCHPEY7SHM6KPA085ES6)) + + (ok true) + ) +) + +;; READ ONLY FUNCTIONS + +(define-read-only (is-executable) + (contract-call? .ccip014-pox-3 is-executable) +) diff --git a/contracts/proposals/ccip014-pox-3.clar b/contracts/proposals/ccip014-pox-3.clar new file mode 100644 index 00000000..ff391a9b --- /dev/null +++ b/contracts/proposals/ccip014-pox-3.clar @@ -0,0 +1,339 @@ +;; 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 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: "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 + +(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: 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)) + + ;; 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 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) + ) +) + +(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) +) + +;; INITIALIZATION + +;; fund proposal with 8 uSTX for payouts from deployer +(stx-transfer? (* MISSED_PAYOUT u8) tx-sender (as-contract tx-sender)) 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)) + ) +) 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) { - 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) { + 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 = [], 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-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 = [], 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 new file mode 100644 index 00000000..59f25208 --- /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_FAILED, +} + +export class CCIP014Pox3 { + 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 called by passProposal and CCD001 + + voteOnProposal(sender: Account, vote: boolean) { + return Tx.contractCall(this.name, "vote-on-proposal", [types.bool(vote)], sender.address); + } + + // read-only functions + + isExecutable() { + return this.callReadOnlyFn("is-executable"); + } + + isVoteActive() { + return this.callReadOnlyFn("is-vote-active"); + } + + getProposalInfo() { + return this.callReadOnlyFn("get-proposal-info"); + } + + getVotePeriod() { + return this.callReadOnlyFn("get-vote-period"); + } + + getVoteTotals() { + return this.callReadOnlyFn("get-vote-totals"); + } + + getVoterInfo(userId: number) { + return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]); + } + + getMiaVote(cityId: number, userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-mia-vote", [types.uint(cityId), types.uint(userId), types.bool(scaled)]); + } + + getNycVote(cityId: number, userId: number, scaled: boolean) { + return this.callReadOnlyFn("get-nyc-vote", [types.uint(cityId), types.uint(userId), types.bool(scaled)]); + } + + // read-only function helper + private callReadOnlyFn(method: string, args: Array = [], sender: Account = this.deployer): ReadOnlyFn { + const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address); + return result; + } +} diff --git a/tests/base-dao.test.ts b/tests/base-dao.test.ts index 9201dc1e..d87fe120 100644 --- a/tests/base-dao.test.ts +++ b/tests/base-dao.test.ts @@ -203,6 +203,11 @@ 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 + 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/external/mock-pox-3.clar b/tests/contracts/external/mock-pox-3.clar new file mode 100644 index 00000000..3e36aa7d --- /dev/null +++ b/tests/contracts/external/mock-pox-3.clar @@ -0,0 +1,1356 @@ +;; 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 + +;; 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) +(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 ) (total-ustx ))) +(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 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/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-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/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/contracts/proposals/test-ccip014-pox-3-001.clar b/tests/contracts/proposals/test-ccip014-pox-3-001.clar new file mode 100644 index 00000000..0bc303ad --- /dev/null +++ b/tests/contracts/proposals/test-ccip014-pox-3-001.clar @@ -0,0 +1,36 @@ +;; 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) + ) +) 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/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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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/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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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(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) { + // 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(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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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 412da868..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"; @@ -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); } } @@ -1046,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); @@ -1143,10 +1146,10 @@ 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"); @@ -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); }, }); @@ -1437,10 +1440,10 @@ 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"); 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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) { + // 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/tests/proposals/ccip014-pox-3.test.ts b/tests/proposals/ccip014-pox-3.test.ts new file mode 100644 index 00000000..02576077 --- /dev/null +++ b/tests/proposals/ccip014-pox-3.test.ts @@ -0,0 +1,702 @@ +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"; + +Clarinet.test({ + name: "ccip-014: execute() fails with ERR_VOTE_FAILED if there are no votes", + fn(chain: Chain, accounts: Map) { + // arrange + + // register MIA and NYC + constructAndPassProposal(chain, accounts, PROPOSALS.TEST_CCD004_CITY_REGISTRY_001); + // set activation details for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_001); + // set activation status for MIA and NYC + passProposal(chain, accounts, PROPOSALS.TEST_CCD005_CITY_DATA_002); + + // act + + // execute ccip-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) { + // 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 ccip014pox3 = 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 + 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) { + // 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 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)]); + + /* 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)); + */ + + // 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); + }, +}); + +Clarinet.test({ + name: "ccip-014: execute() succeeds if there are more yes than no votes", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const ccd007CityStacking = new CCD007CityStacking(chain, sender, "ccd007-citycoin-stacking"); + const 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 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([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) { + // 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 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([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) { + // 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 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // act + + // execute yes and no vote + const votingBlock = chain.mineBlock([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) { + // 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 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([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) { + // 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 5 + // votes are counted in cycles 2-3 + // past payouts tested for cycles 1-4 + chain.mineEmptyBlockUntil(CCD007CityStacking.REWARD_CYCLE_LENGTH * 6 + 10); + ccd007CityStacking.getCurrentRewardCycle().result.expectUint(5); + + // execute yes and no vote + // user 1 has more voting power + const votingBlock = chain.mineBlock([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) { + // 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 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([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); + }, +}); + +Clarinet.test({ + name: "ccip-014: after upgrade mining disabled, mining-v2 enabled, mining and stacking claims work as expected", + fn(chain: Chain, accounts: Map) { + // arrange + const sender = accounts.get("deployer")!; + const user1 = accounts.get("wallet_1")!; + const user2 = accounts.get("wallet_2")!; + const ccd006CityMining = new CCD006CityMining(chain, sender, "ccd006-citycoin-mining"); + const 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 = 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); + + // 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([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); + + // 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); + }, +}); diff --git a/utils/common.ts b/utils/common.ts index 9af02cd7..cf496c12 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -1,11 +1,15 @@ 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 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"; @@ -14,13 +18,16 @@ 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"), 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"), @@ -30,6 +37,8 @@ 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"), + 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"), @@ -45,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"), @@ -69,11 +90,19 @@ 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"), 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_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"), @@ -84,6 +113,8 @@ 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"), + TEST_CCIP014_POX3_002: ADDRESS.concat(".test-ccip014-pox-3-002"), }; export const EXTERNAL = { @@ -124,3 +155,80 @@ export const constructAndPassProposal = (chain: Chain, accounts: Map): 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 = { + 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", +}; 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";